Drag and drop
How tabs are dragged and dropped, which command a drop runs, and how to style the drop indicator and the dragged tab.
Drag and drop works out of the box: every Dockable.Tab is draggable (when its tab's
draggable allows it), and every tabset, tab strip and layout edge is a drop target. What you
add is the look: the drop indicator, and the dragged tab's state.
What happens during a drag
- A tab uses native HTML5 drag and drop:
draggable,dragstart,dragend. The browser snapshots the draggedTabelement as the drag image (no text is added). The drag carries the MIME typeDRAG_TYPE(application/x-dockable), so a layout only claims its own drags. - The engine listens for native
dragenter/dragover/dragleave/dropon the root (not React events, so the same path works in a popout document) and hit-tests the pointer against the measured rects. - Each candidate target becomes the command the drop would run, and the engine asks the model
with
model.can(a dry run: nothing changes). The first accepted candidate is the target, and the drop indicator state updates: where the tab would land and how. - On drop, the engine runs that command with
model.run:tab.movewith{ tabId, to, location, index }(ortabset.movefor a tabset,tab.addfor a new tab). A middleware sees it like any other command; veto it there and nothing moves.
| drop on | location | result |
|---|---|---|
| a tab strip | center | the tab joins the tabset at the pointer's index |
| the middle of a tabset's content | center | the tab joins the tabset (appended) |
| a tabset's edge | top, bottom, start, end | the tabset splits; the tab gets a new tabset on that side |
| the layout's outer edge | top, bottom, start, end (kind: "edge") | a new tabset along the whole edge |
Tabsets can be dropped too (tabset.move), but the React primitives start drags only from tabs
today. useDragNode(node) takes a tab or a tabset node and returns the props that wire an
element to the drag machine, if you build your own tabset drag handle.
The drop indicator
Dockable.DropIndicator is one element the engine positions over the drop target
(structurally: position: absolute, left/top/width/height, display: none while there is
no target, and pointer-events: none). Place it anywhere inside the root and style it:
<Dockable.DropIndicator
className="z-10 rounded-md border-2 border-palette-ring bg-palette-ring/15 data-[drop-kind=edge]:border-dashed"
style={(state) => ({
transitionProperty: "left, top, width, height",
transitionDuration: `${state.tabDragSpeed}s`,
})}
/>Its state (also as attributes):
| state | attribute | meaning |
|---|---|---|
visible | data-visible | a drop target is under the pointer |
location | data-drop-location (while visible) | center, top, bottom, start or end |
kind | data-drop-kind (while visible) | edge for a layout-edge drop, rect otherwise |
dragging | data-dragging | a drag is over this layout |
showEdges | none | edge docking is available for this drag |
tabDragSpeed | none | seconds a style may take to animate between targets (tabDragSpeed on Root, default 0.3) |
refused | data-drop-refused | the pointer is over a target the model refuses |
targetNodeId | none | the drop target node: a tabset, a row for an edge drop, a border |
targetTabsetId | none | the tabset (or border) the drop goes into or beside |
The core never animates: the transition is yours, and tabDragSpeed only tells you the duration
the layout was configured with.
Since className takes the state, the indicator can say where a tab would land. location and
kind give the side and whether the drop is at the layout's edge, and targetTabsetId the tabset
the drop goes into or beside (that tabset also has data-drop-target). The example gives each
region its colour and each kind of drop its fill:
Stacking: give it a z-index
Panels are portalled into the root after its other children. Absolutely positioned siblings
without a z-index paint in DOM order, so an indicator without one paints under the panels
and is hidden over tab content. Give it a z-index (z-10 above). This is gap 12 in
Limitations.
pointer-events: none is structural on purpose: an indicator under the pointer would take the
drag's enter and leave events from the layout, and the browser would cancel the drop.
Styling the drag
- The dragged tab has
data-draggingwhile it is dragged: fade it (data-dragging:opacity-40). - The root has
data-draggingwhile any node of the layout is dragged: for example to show drop hints or disable hover effects. - The drag image is the browser's snapshot of the
Tabelement, styled as it is when the drag starts.
Layout rules that affect drops
- Keep tab strips at least ~30px tall, or lower
edgeDockMargin. The layout's top edge has a 10px docking band (defaults.layout.edgeDockMargin); a thin strip at the top of the layout falls inside it, and drops aimed at the strip dock to the layout edge instead (gap 11).Dockable.EdgeIndicatorshows the bands during a drag (Borders). - Edge docking can be turned off in the layout's defaults,
defaults.layout.edgeDock: false(or at run time withlayout.configure).
Dropping between windows and layouts
Tabs drag between the main window and its popouts, and between popouts, with no set-up. Two
separate Dockable.Roots (different models) exchange tabs when a Dockable.DragGroup wraps them.
See Drag across windows and layouts. Dragging things from
outside the layout into it (a widget sidebar, files) is covered in
External drag.
Controlling where drops go
Node flags (draggable, droppable, splittable, and pinned tabs) and a middleware on
tab.move, tabset.move and tab.add decide what may be dropped where. The indicator refuses a
target exactly when model.can refuses its command, so both show the same way; see
Restricting drops.