• CMSnpm version

    • Overview
    • ContentBuilder
    • InviteUserForm
    • LoginForm
    • RenewPasswordForm
    • WysiwygInput
  • UInpm version

    • Overview
    • Accordion
    • AlertBubble
    • AnnouncementBar
    • Avatar
    • AvatarFileInput
    • Badge
    • Button
    • ButtonGroup
    • ButtonList
    • Calendar
    • Checkbox
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • Chip
    • ColorRadio
    • ColorRadioGroup
    • Combobox
    • DatePicker
    • DatePickerInput
    • DateRangePicker
    • DateRangePickerInput
    • DatetimePicker
    • DatetimePickerInput
    • Dialog
    • Dropdown
    • Dropzone
    • ErrorMessage
    • FileInput
    • FlashMessages
    • FormComponent
    • Icon
    • IconButton
    • ImageGallery
    • InfoBox
    • Input
    • Label
    • Layout
    • Lightbox
    • ListItem
    • Loader
    • Lozenge
    • Menu
    • Message
    • Modal
    • ✅ ModalDialog
    • ✅ ModalHeader
    • MultiCombobox
    • MultiSelect
    • Pagination
    • Paper
    • Popover
    • Radio
    • RadioGroup
    • RasterImage
    • Select
    • ✅ Tabs
    • TextInput
    • TextLink
    • Textarea
    • TimePicker
    • TimePickerInput
    • Toggle
    • Tooltip
    • Typography
  • Formnpm version

    • Overview
    • AvatarFileInput
    • CheckboxButton
    • CheckboxInput
    • CheckboxList
    • ColorRadioGroup
    • Combobox
    • DatePickerInput
    • DateRangePickerInput
    • DatetimePickerInput
    • Dropzone
    • FileInput
    • Form
    • FormRenderer
    • GpsInput
    • MoneyInput
    • MultiCombobox
    • MultiSelect
    • NumberInput
    • PasswordInput
    • RadioGroup
    • Select
    • TextInput
    • Textarea
    • TimePickerInput
    • Toggle
  • DataGridnpm version

    • Overview
    • DataGrid
    • DataGridCustomExample
    • ExportButton
    • FilterList
    • Filters
    • FiltersButton
    • FulltextInput
    • HiddenColumns
    • HiddenColumnsButton
    • Pagination
    • RowCounts
    • RowsPerPageSelect
    • SelectedRowsToolbar
    • TableV2
    • ToolbarControl
    • ToolbarCustoms
    • ToolbarTabs
  • Wysiwygnpm version

    • Overview
  • Resizernpm version

    • Overview
  • Routernpm version

    • Overview
  • Corenpm version

    • Overview
  • Core-Reactnpm version

    • Overview
  • DnDnpm version

    • Overview
  • Stylesnpm version

    • Overview
  • Localizenpm version

    • Overview
  • Analyticsnpm version

    • Overview
  • Datepickernpm version

    • Overview
  • Icons-generatornpm version

    • Overview
  • Smart-addressnpm version

    • Overview
  • E2Enpm version

    • Overview
  • E2E-Playwrightnpm version

    • Overview

@uxf/dnd

Drag and drop for UXF apps: a curated facade over @dnd-kit plus the sortable-list hooks shared by @uxf/data-grid and @uxf/cms.

When to use

Reach for this package whenever an app needs drag and drop. It exists so that a consumer declares one dependency instead of three @dnd-kit/* packages, and so that @dnd-kit is installed only where drag and drop is actually rendered.

It is not a UI package: it ships hooks and re-exports, no components and no CSS. The draggable markup stays in your app (see @uxf/data-grid/hidden-columns for a worked example).

Before this package existed the same hooks lived in @uxf/core-react/dnd, which forced @dnd-kit onto every consumer of @uxf/core-react. Importing them from @uxf/core-react no longer works — see docs/migration/dnd-package-extraction.md.

Installation

yarn add @uxf/dnd @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

Peer dependencies: @dnd-kit/core ^6.3.1, @dnd-kit/sortable ^10.0.0, @dnd-kit/utilities ^3.2.2, react >=18.2.0, react-dom >=18.2.0.

No CSS and no provider setup. The whole package is client-side; index.ts carries "use client".

Quick start

A single sortable list, wired with useSortableSingle:

"use client";

import {
    CSS,
    DndContext,
    DragOverlay,
    SortableContext,
    useSortable,
    useSortableSingle,
    verticalListSortingStrategy,
} from "@uxf/dnd";
import React, { CSSProperties } from "react";

interface Item {
    id: string;
    label: string;
}

function Row(props: Item) {
    const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props.id });
    const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition };

    return (
        <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
            {props.label}
        </div>
    );
}

export function SortableList(props: { items: Item[] }) {
    const { items, activeItem, onDragStart, onDragEnd, onDragCancel, sensors, dropAnimationConfig } = useSortableSingle(
        props.items,
    );

    return (
        <DndContext onDragCancel={onDragCancel} onDragEnd={onDragEnd} onDragStart={onDragStart} sensors={sensors}>
            <SortableContext items={items.map((i) => i.id)} strategy={verticalListSortingStrategy}>
                {items.map((item) => (
                    <Row key={item.id} {...item} />
                ))}
            </SortableContext>
            <DragOverlay dropAnimation={dropAnimationConfig}>{activeItem ? <Row {...activeItem} /> : null}</DragOverlay>
        </DndContext>
    );
}

API

Everything is exported from the package root, @uxf/dnd. The hooks are also importable one by one from @uxf/dnd/hooks/<name>.

Hooks

HookSignatureDescription
useSortableCore() => { activeElement, setActiveElement, onDragStart, onDragCancel, sensors, dropAnimationConfig }The shared pieces every list needs: pointer + keyboard sensors, the active-element state, and a drop animation that fades the dragged item to 0.4.
useSortableSingle<T extends { id: string }>(initialItems: T[], onReorder?: (fromIndex, toIndex, updatedList: T[]) => void) => …One list. Owns the item array, applies arrayMove on drop and calls onReorder. Returns items, itemsIds and setItems on top of the useSortableCore result.
useSortableMulti<T extends { id: string }, SectionMap extends Record<string, T[]>>(sections, setSections, onMove?) => …Several lists with items moving between them. State stays with the caller; the hook computes the next SectionMap and calls onMove.

useSortableMulti also exports the two placeholder ids it drops onto empty sections: EMPTY_VISIBLE_COLUMNS_PLACEHOLDER_ID and EMPTY_HIDDEN_COLUMNS_PLACEHOLDER_ID. Render one as a droppable whenever a section is empty, otherwise there is no drop target to aim at. getSectionItemsIds(section) returns the right id for you.

Re-exported from @dnd-kit/core

DndContext, DragOverlay, KeyboardSensor, MouseSensor, PointerSensor, TouchSensor, closestCenter, closestCorners, defaultDropAnimationSideEffects, pointerWithin, rectIntersection, useDndContext, useDndMonitor, useDraggable, useDroppable, useSensor, useSensors.

Types: Active, CollisionDetection, DragCancelEvent, DragEndEvent, DragMoveEvent, DragOverEvent, DragStartEvent, DraggableAttributes, DropAnimation, Modifier, Modifiers, Over, UniqueIdentifier.

Re-exported from @dnd-kit/sortable

SortableContext, arrayMove, arraySwap, horizontalListSortingStrategy, rectSortingStrategy, rectSwappingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy.

Types: AnimateLayoutChanges, SortingStrategy.

Re-exported from @dnd-kit/utilities

CSS, and the Transform type.

Gotchas

  • Import from @uxf/dnd, never from @dnd-kit/* directly. dnd-kit keeps its state in a React context, so two copies in one tree fail silently: drags start but never find a drop target. Going through the facade keeps a single version resolved for the whole app.
  • Items must carry a string id. useSortableSingle and useSortableMulti both constrain T to { id: string }, and SortableContext is fed those same ids.
  • useSortableMulti's empty-section placeholders assume sections named visible and hidden. Dropping onto a placeholder resolves the destination to one of those two literal keys, so a SectionMap keyed differently will move the item into a section that does not exist. Non-empty sections work with any keys.
  • When the order lives elsewhere, render your own list and take only itemsIds and activeItem. That is what @uxf/cms does: it renders react-hook-form's fields and feeds onReorder's first two arguments straight into the field array's move(). The hook's own items copy is then just the bookkeeping behind those two values.
  • Client components only. Every hook uses useState, so the calling module needs "use client" under the Next.js app router.

Links

  • dnd-kit documentation
  • @uxf/data-grid/hidden-columns — useSortableMulti in production
  • @uxf/cms/pages/content-builder — useSortableSingle driving a react-hook-form field array