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

Dockable.Root

The layout's root owns the engine for a model and is the containing block the panels are positioned in.

"The layout's root: owns the engine for model, attaches it to the rendered element (the containing block the panels are positioned in), runs the measure-and-position cycle after every commit and re-renders when the engine asks."

const model = createModel<Types>(json);

<Dockable.Root model={model} className="layout">
    <Dockable.Row<Types>>{renderNode}</Dockable.Row>
    <Dockable.Panels<Types>>
        {(tab) => <Dockable.Panel node={tab}>{content(tab)}</Dockable.Panel>}
    </Dockable.Panels>
    <Dockable.DropIndicator />
    <Dockable.Popout<Types>>{() => <Dockable.Row<Types>>{renderNode}</Dockable.Row>}</Dockable.Popout>
</Dockable.Root>

The root has no intrinsic size; give it one (Sizing the root).

Root is generic over the model's registry: model: Model<Types> types the callbacks (onExternalDrag, popoutTitle, onPopoutOpen, onPopoutClose). The parts that hand nodes to a children function (Row, TabList, Panels, Borders, Popout, TabOverflowTrigger) cannot infer it through context: pass it as their type argument (<Dockable.Panels<Types>>).

The layout's policies and reactions live on the model, not on the root: a middleware (model.use) sees, rewrites or vetoes every command the layout runs (a drop, a close, a splitter release), and a listener (model.subscribe) sees every change. See The command bus.

Props

RootProps<T> extends the common primitive props.

proptypedefaultdescription
modelModel<T>requiredthe layout model. Its middleware (model.use) sees every command the layout runs, and its listeners (model.subscribe) every change; a new state never needs a new model (layout.load). A new model identity creates a new engine
keyMapKeyMapdefaultKeyMapkeyboard bindings, merged over defaultKeyMap
realtimeResizebooleantruetrue (default) to resize live while dragging a splitter; false to preview and commit on release
tabDragSpeednumber0.3seconds a view may take to animate the drop indicator (exposed as data; default 0.3)
popoutURLstring"popout.html"the popout host page (default "popout.html"); the window layout's id is passed as ?id=
supportsPopoutbooleana desktop pointer is presentwhether window layouts open as popouts; without support, a window layout's tabs dock back into the main layout
popoutTitle(layout: WindowLayout<T>) => string | undefinednonea popout document's title; with none, the host page's title is kept
onPopoutOpenPopoutCallback<T>: (layout: WindowLayout<T>, window: Window, document: Document) => voidnonea popout document is ready, before its content renders
onPopoutClosePopoutCallback<T>nonea popout window is closing (its tabs dock back into the main layout)
openWindowOpenWindow: (url: string, name: string, features: string) => Window | nullthe main window's openopens a popout's native window. Return null to refuse: the window layout docks back
popoutMirrorRootboolean | readonly string[]none (only lang; dir is always the layout's direction)copies the main document's <html> and <body> attributes into each popout and keeps them in sync (a theme class, data-theme, …): true copies them all (except style, id and dir), a list copies those names
onExternalDragOnExternalDrag<T>: (event: DragEventLike) => { tab: TabInitOf<T>; onDrop?: NewTabDropped } | undefinednoneaccepts a drag that did not start in a layout (files, links, text, another library's element) as a new tab: return { tab, onDrop? }, or undefined to ignore it. Called when the drag enters the layout, when only event.dataTransfer.types is readable; read the data in onDrop(tabId | undefined, event). A native drag that starts in the layout's own content is not asked about. See External drag
childrenReactNodenonethe layout: a Row, Panels, DropIndicator, Popout, and anything else

KeyMap is described in Keyboard. The root renders no text and names nothing: see Accessible names.

The tab onExternalDrag returns is a typed tab.add init (TabInitOf<T>): data is checked against component. The drop runs tab.add through the model's middleware, so a veto refuses it like any other drop.

<Dockable.Root
    model={model}
    onExternalDrag={(event) =>
        event.dataTransfer?.types.includes("Files")
            ? {
                  tab: { component: "file", label: "Dropped file" },
                  onDrop: (tabId, drop) => {
                      const file = drop.dataTransfer?.files[0];
                      if (tabId && file) {
                          model.run("tab.rename", { tabId, label: file.name });
                      }
                  },
              }
            : undefined
    }
>

State

RootState, passed to render, className and style functions.

fieldtypedescription
maximizedbooleana tabset of the main layout is maximized
draggingbooleana node of this layout is being dragged
refusedbooleanthe drag is over a target of the main layout that a drop rule refused

Data attributes

attributewhen
data-layout-pathalways "/layout"
data-maximizeda tabset of the main layout is maximized
data-dragginga node of this layout is being dragged
data-drop-refusedthe drag is over a target that a drop rule (or a middleware veto) refused

Structural style

position: relative: the containing block the panels (and the root row) are positioned in.

Behaviour

  • Creates one engine per model identity and attaches it to the element on mount.
  • After every commit, runs engine.run("measure-and-position") (measure, then position the panels).
  • Opens a popout window for every window layout of the model's state while it is mounted (see Dockable.Popout).
  • Listens on its document for the focusNextTabset/focusPreviousTabset bindings, and closes an open overlay border on a press elsewhere or on its close key.
  • Provides the context useDockable reads.

Common props

Every primitive except Panels accepts these on top of its own props (PrimitiveProps<State> and the div attributes, as DivPrimitiveProps<State>):

proptypedescription
renderRenderProp<State>: ReactElement | ((props: RenderedProps, state: State) => ReactElement)replaces the rendered element (never asChild)
classNamestring | ((state: State) => string | undefined)a class name, or a function of the primitive's state returning one
styleCSSProperties | ((state: State) => CSSProperties | undefined)a style, or a function of the primitive's state returning one. Structural keys the primitive sets (position, geometry, display, flex sizing) always win
refRef<HTMLElement>merged with the primitive's own ref
any div attributeforwarded to the element; event handlers run after the primitive's own

RenderedProps<E extends Element = HTMLElement> is what a render function receives: the element's attributes, the primitive's data-* attributes and a ref. Spread them onto the element it returns. The ref is a callback ref, (element: E | null) => void | (() => void), so it fits any element's ref with no cast:

<Dockable.TabSet
    node={tabset}
    render={(props, state) => (
        <section {...props} aria-label={state.active ? "Active tabset" : undefined} />
    )}
/>

The exported types are PrimitiveProps<State>, DivPrimitiveProps<State>, ButtonPrimitiveProps<State> (the button attributes instead, for PopoutTrigger and TabOverflowTrigger), RenderProp<State> and RenderedProps<E>. See The primitive contract.