Dockable
API reference

Hooks

useDockable, useTabSet, useBorder, useTabOverflow, useSplitter, useDragNode, useDragSource, useDropZone and useTabSetDropState - the lower layer under the primitives.

The primitives are built on four hooks, exported as the lower layer. Use useDockable all the time (it is how components inside the layout dispatch actions); use the others only when a primitive's element does not fit your markup.

All of them must be called inside Dockable.Root.

useDockable

"The lower layer: the engine, model and layout of the enclosing Dockable.Root. Dispatch actions with engine.doAction(Actions.x(...)) so they go through onAction."

import { Actions } from "@fragiola/dockable";
import { useDockable } from "@fragiola/dockable-react";

function ResetButton() {
    const { engine, model } = useDockable();
    const first = model.getFirstTabSet();
    return (
        <button type="button" onClick={() => first && engine.doAction(Actions.selectTab(first.getTabNodes()[0]?.getId() ?? ""))}>
            Back to the first tab
        </button>
    );
}

useDockable(): UseDockableResult

fieldtypedescription
engineLayoutEnginethe engine of the layout this component renders in (the main layout or a popout's)
mainEngineLayoutEnginethe main layout's engine
modelModelthe model
layoutIdstringthe id of the layout this component renders in
getLabelGetLabel | undefinedthe getLabel resolver given to Dockable.Root

Inside tab content, engine is the engine of the window the tab is currently in; use mainEngine for window-level things (isSupportsPopout(), moving a tab back to the main layout).

useTabSet

"The lower layer of Dockable.TabSet: its state, measurement ref and activation handler."

function MyTabSet({ node, children }: { node: TabSetNode; children: ReactNode }) {
    const { state, ref, onPointerDown } = useTabSet(node);
    return (
        <section ref={ref} onPointerDown={onPointerDown} data-active={state.active ? "" : undefined}>
            {children}
        </section>
    );
}

A custom tabset must still apply the structural flex sizing Dockable.TabSet sets (flex-grow from node.getWeight(), the min/max sizes, display: none when state.hidden), and provide the tabset to TabList/TabSetContent, which only Dockable.TabSet does. Prefer Dockable.TabSet with render unless you need something it cannot express.

useTabSet(node: TabSetNode): UseTabSetResult

fieldtypedescription
stateTabSetStatesee below
refRefCallback<HTMLElement>callback ref for the tabset's element (measured as the tabset)
onPointerDown(event: PointerEvent<HTMLElement>) => voidmakes the tabset active on a primary pointer press

TabSetState:

fieldtypedescription
activebooleanthe tabset is the model's active tabset
maximizedbooleanthe tabset is maximized
hiddenbooleananother tabset of the layout is maximized, so this one is hidden
emptybooleanthe tabset has no tabs
dropTargetbooleanthe current drag would drop into or beside this tabset (its content, strip or a group)
dropLocationDropLocation | undefinedwhile it is the drop target: where the drag would dock relative to it
dropRefusedbooleanthe current drag is over this tabset, but a drop rule refuses it

useBorder

"The lower layer of Dockable.Border: its state and measurement ref."

useBorder(node: BorderNode): UseBorderResult

fieldtypedescription
stateBorderStatesee Dockable.Border
refRefCallback<HTMLElement>callback ref for the border's strip (measured as the border's tab header)

A custom strip must also provide the border to Dockable.TabList, which only Dockable.Border does: prefer Dockable.Border with render.

useTabOverflow

"Tab overflow of a tabset or a border: which of its tabs are hidden because they do not fit in its Dockable.TabList (the engine measures the list, the tabs and the Dockable.TabOverflowTrigger). The selected tab is never hidden."

useTabOverflow(container: TabSetNode | BorderNode): UseTabOverflowResult

fieldtypedescription
overflowingbooleansome of the container's tabs do not fit, so they are hidden
hiddenTabNode[]the hidden tabs, in model order: what an overflow menu lists
visibleTabNode[]the tabs that stay in the strip, in model order

It re-renders only when the container's hidden tabs change. See Tab overflow.

useSplitter

"The lower layer of Dockable.Splitter: a headless controller for the splitter before child index (1-based) of node, its drag state and ARIA values."

function MySplitter({ node, index }: RowSplitterProps) {
    const { controller, state, aria, hidden, ref } = useSplitter(node, index);
    if (hidden) return null;
    return (
        <div
            ref={ref}
            role="separator"
            tabIndex={0}
            aria-orientation={aria.orientation}
            aria-valuenow={aria.valueNow}
            aria-valuetext={aria.valueText}
            data-dragging={state.dragging ? "" : undefined}
            onPointerDown={(event) => controller.onPointerDown(event.nativeEvent)}
            onKeyDown={(event) => controller.onKeyDown(event.nativeEvent)}
        />
    );
}

useSplitter(node: RowNode | BorderNode, index = 0): UseSplitterResult (a border's splitter has no index)

fieldtypedescription
controllerSplitterControllerthe headless controller: onPointerDown, onKeyDown, getAria, isHidden, …
stateISplitterStatedragging and previewOffset (see Splitter)
ariaISplitterAriaorientation, valueNow, valueMin, valueMax, valueText
hiddenbooleanrow splitters are hidden while a tabset is maximized
refRefCallback<HTMLElement>callback ref for the splitter's element

useDragNode

"The lower layer of a draggable part: wires a node (a tab, tabset or group) to the core's drag-and-drop machine. Spread draggable, onDragStart and onDragEnd on the element and attach ref to the element the browser should snapshot as the drag image."

function TabSetDragHandle({ tabset }: { tabset: TabSetNode }) {
    const drag = useDragNode(tabset);
    return (
        <span
            ref={drag.ref}
            draggable={drag.draggable}
            onDragStart={drag.onDragStart}
            onDragEnd={drag.onDragEnd}
            aria-hidden="true"
            data-dragging={drag.dragging ? "" : undefined}
        >
            ⠿
        </span>
    );
}

useDragNode(node): UseDragNodeResult, where node is a Node that is IDraggable and has isEnableDrag() (a TabNode or a TabSetNode).

fieldtypedescription
draggablebooleanwhether the element is draggable: the node's enableDrag
onDragStart(event: DragEvent<HTMLElement>) => voidstarts the drag in the core
onDragEnd(event: DragEvent<HTMLElement>) => voidends it
refRefCallback<HTMLElement>callback ref for the element used as the drag image (the dragged element by default)
draggingbooleanthis node is being dragged

useDragSource

"The lower layer of Dockable.DragSource: turns any element, inside or outside the layout (a sidebar item, a palette entry), into a source of new tabs. Dropping it on the layout dispatches Actions.addTab(json, …) through the engine, so onAction and onAllowDrop apply. Spread draggable, onDragStart and onDragEnd on the element."

function PaletteEntry({ model }: { model: Model }) {
    const drag = useDragSource({
        model,
        json: { type: "tab", name: "Orders", component: "table" },
    });
    return (
        <li
            ref={drag.ref}
            draggable={drag.draggable}
            onDragStart={drag.onDragStart}
            onDragEnd={drag.onDragEnd}
            data-dragging={drag.dragging ? "" : undefined}
        >
            Orders table
        </li>
    );
}

useDragSource(options: UseDragSourceOptions): UseDragSourceResult. It does not need to run inside Dockable.Root.

UseDragSourceOptions:

fieldtypedescription
modelModelthe model of the layout the new tab is dropped into (its Dockable.Root must be mounted)
jsonIJsonTabNode | (() => IJsonTabNode)the tab a drop creates. A function is called at each drag start, so every drop can get a fresh name or config
onDrop(node: TabNode | undefined, event: DragEvent) => voidcalled after the drop with the created tab, or undefined when onAction vetoed it
disabledbooleanno drag starts while true

UseDragSourceResult:

fieldtypedescription
draggablebooleanwhether the element is draggable
onDragStart(event: DragEvent<HTMLElement>) => voidstarts the drag in the core (cancels it when disabled or no layout is mounted)
onDragEnd(event: DragEvent<HTMLElement>) => voidends it
refRefCallback<HTMLElement>callback ref for the element used as the drag image (the dragged element by default)
draggingbooleana drag started by this source is in progress

useDropZone

"The lower layer of Dockable.DropZone: makes an element, inside or outside the layout, a place where a drag of the layout can be dropped for the consumer to handle (a trash can, an "open to the right" pad). Attach ref to the element."

const zone = useDropZone({
    model,
    onDrop: (node) => engine.doAction(Actions.deleteTab(node.getId())),
});
return <div ref={zone.ref} data-over={zone.over ? "" : undefined}>Trash</div>;

useDropZone(options: UseDropZoneOptions): UseDropZoneResult. It does not need to run inside Dockable.Root.

UseDropZoneOptions:

fieldtypedescription
modelModelthe model whose drags the zone takes
accepts(dragNode: Node) => booleanwhether the zone takes this drag (default: every drag of the model)
onDrop(dragNode: Node, event: DragEvent) => voidcalled when the drag is dropped on the zone, with the dragged node. Nothing is moved: dispatch the action you want

UseDropZoneResult:

fieldtypedescription
refRefCallback<HTMLElement>callback ref for the zone's element
overbooleana drag the zone takes is over it
activebooleana drag the zone would take is in progress

useTabSetDropState

useTabSetDropState(engine, tabsetId): TabSetDropState: whether the current drag targets (or is refused by) a tabset, from the engine's indicator state. It re-renders only when the answer for that tabset changes, not on every pointer move. Dockable.TabSet and Dockable.TabList use it for their data-drop-* attributes; use it for anything they do not cover (an insertion caret between two tabs).

TabSetDropState:

fieldtypedescription
targetbooleanthe current drag would drop into or beside this tabset
locationDropLocation | undefinedwhile it is the target: where the drag would dock
stripbooleanwhile it is the target: the drop goes into its tab strip, at index
indexnumberfor a strip drop: the insertion index among the tabset's children, else -1
refusedbooleanthe current drag is over this tabset, but a drop rule refuses it

Types

Also exported: TabSetState, UseDockableResult, UseTabSetResult, UseBorderResult, UseTabOverflowResult, UseSplitterResult, UseDragNodeResult, UseDragSourceOptions, UseDragSourceResult, UseDropZoneOptions, UseDropZoneResult, TabSetDropState, and GetLabel ((key: DockableLabel, ...args: (string | number)[]) => string | undefined).

On this page