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).
| prop | type | default | description |
|---|---|---|---|
model | Model<T> | required | the model of the layout the new tab is dropped into (its Dockable.Root must be mounted) |
tab | TabInitOf<T> | (() => TabInitOf<T>) | required | the 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 |
onDrop | NewTabDropped: (tabId: string | undefined, event: DragEventLike) => void | none | called after the drop with the new tab's id, or undefined when the add was refused (a drop rule, a middleware veto) |
disabled | boolean | false | no drag starts while true |
children | ReactNode | none | the 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:
| field | type | description |
|---|---|---|
dragging | boolean | a drag started by this source is in progress |
disabled | boolean | the source is disabled |
Data attributes and ARIA
| attribute | value |
|---|---|
data-layout-path | none: 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-dragging | a drag started by this source is in progress |
data-disabled | the source is disabled |
aria-disabled | true while disabled |
draggable | true, 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-draggingon it to show which one is moving. useDragSourceis 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
onExternalDragonDockable.Root.