Documentation
API: primitives (@fragiola/dockable-react)

Dockable.DragSource

An element anywhere on the page that can be dragged into a layout to create a new tab, through the model's middleware.

"An element, anywhere on the page, that can be dragged into a layout to create a new tab: a sidebar of widgets, a file list, a palette. The drop runs tab.add through the model's middleware, which can veto or rewrite it. It is itself the drag image. It renders a div with draggable; give it a role and a name that fit your UI, and offer a keyboard path to the same command (a click, a menu): native drag and drop has none."

<Dockable.DragSource
    model={model}
    tab={() => ({ component: "chart", label: "Revenue", data: { series: [] } })}
    onDrop={(tabId) => tabId && console.log("added", tabId)}
    render={<li />}
    className="cursor-grab data-dragging:opacity-50"
>
    Revenue chart
</Dockable.DragSource>

It does not have to live inside Dockable.Root: it finds the layout through its model (the root rendering that model must be mounted; before that, a drag does not start). T is inferred from model: Model<T>, so the tab init is checked against the registry: data must be the data type of its component.

Props

DragSourceProps<T> extends the common primitive props (its onDrop replaces the element's native drop handler: a drag source is never a drop target).

proptypedefaultdescription
modelModel<T>requiredthe model of the layout the new tab is dropped into (its Dockable.Root must be mounted)
tabTabInitOf<T> | (() => TabInitOf<T>)requiredthe tab a drop creates (a tab.add init: data is checked against component); a function is called at each drag start, so every drop can get fresh data
onDropNewTabDropped: (tabId: string | undefined, event: DragEventLike) => voidnonecalled after the drop with the new tab's id, or undefined when the add was refused (a drop rule, a middleware veto)
disabledbooleanfalseno drag starts while true
childrenReactNodenonethe source's content; the primitive renders no text of its own

The init is what tab.add takes without its placement (to, location, index): the drop supplies those. Read the new tab back with model.get("node-by", { id: tabId }).

State

DragSourceState:

fieldtypedescription
draggingbooleana drag started by this source is in progress
disabledbooleanthe source is disabled

Data attributes and ARIA

attributevalue
data-layout-pathnone: a drag source is not part of a layout, so it has no layout path (select it by your own data-testid or role)
data-dragginga drag started by this source is in progress
data-disabledthe source is disabled
aria-disabledtrue while disabled
draggabletrue, or false while disabled

While its drag is over a layout, that layout's Dockable.Root has data-dragging too.

Accessibility

Native drag and drop has no keyboard path. Give the source a role and a name that fit your UI (render={<button />} makes it focusable), and offer the same command without a drag: a click that adds the tab to the active tabset.

const addToActiveTabset = () => {
    const target = model.get("default-tabset");
    if (!target) return;
    const added = model.run("tab.add", { ...widgetTab(widget), to: target.id, select: true });
    onAdded(added.ok ? added.value.tabId : undefined);
};

model.run works anywhere, with or without a mounted root, and goes through the same middleware as the drop. The widget sidebar example does both.

Notes

  • The drag image is the source element. Style data-dragging on it to show which one is moving.
  • useDragSource is the lower layer, for an element you render yourself: see hooks.
  • Foreign drags (files from the OS, links, another library's elements) do not need a source: see onExternalDrag on Dockable.Root.