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.
| prop | type | default | description |
|---|---|---|---|
model | Model<T> | required | the 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 |
keyMap | KeyMap | defaultKeyMap | keyboard bindings, merged over defaultKeyMap |
realtimeResize | boolean | true | true (default) to resize live while dragging a splitter; false to preview and commit on release |
tabDragSpeed | number | 0.3 | seconds a view may take to animate the drop indicator (exposed as data; default 0.3) |
popoutURL | string | "popout.html" | the popout host page (default "popout.html"); the window layout's id is passed as ?id= |
supportsPopout | boolean | a desktop pointer is present | whether window layouts open as popouts; without support, a window layout's tabs dock back into the main layout |
popoutTitle | (layout: WindowLayout<T>) => string | undefined | none | a popout document's title; with none, the host page's title is kept |
onPopoutOpen | PopoutCallback<T>: (layout: WindowLayout<T>, window: Window, document: Document) => void | none | a popout document is ready, before its content renders |
onPopoutClose | PopoutCallback<T> | none | a popout window is closing (its tabs dock back into the main layout) |
openWindow | OpenWindow: (url: string, name: string, features: string) => Window | null | the main window's open | opens a popout's native window. Return null to refuse: the window layout docks back |
popoutMirrorRoot | boolean | 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 |
onExternalDrag | OnExternalDrag<T>: (event: DragEventLike) => { tab: TabInitOf<T>; onDrop?: NewTabDropped } | undefined | none | accepts 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 |
children | ReactNode | none | the 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.
| field | type | description |
|---|---|---|
maximized | boolean | a tabset of the main layout is maximized |
dragging | boolean | a node of this layout is being dragged |
refused | boolean | the drag is over a target of the main layout that a drop rule refused |
Data attributes
| attribute | when |
|---|---|
data-layout-path | always "/layout" |
data-maximized | a tabset of the main layout is maximized |
data-dragging | a node of this layout is being dragged |
data-drop-refused | the 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
modelidentity 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/focusPreviousTabsetbindings, and closes an open overlay border on a press elsewhere or on its close key. - Provides the context
useDockablereads.
Common props
Every primitive except Panels accepts these on top of its own props (PrimitiveProps<State>
and the div attributes, as DivPrimitiveProps<State>):
| prop | type | description |
|---|---|---|
render | RenderProp<State>: ReactElement | ((props: RenderedProps, state: State) => ReactElement) | replaces the rendered element (never asChild) |
className | string | ((state: State) => string | undefined) | a class name, or a function of the primitive's state returning one |
style | CSSProperties | ((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 |
ref | Ref<HTMLElement> | merged with the primitive's own ref |
any div attribute | forwarded 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.