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

Dockable.Popout

The popout windows. The core opens a native window per window layout of the state, and this part portals your layout into it.

"The popout windows. Place it once, directly under Dockable.Root, with the model's registry as its type argument (<Dockable.Popout<Types>>). The core keeps a native window open for every window layout of the state; once one is ready (styles copied), the child function's result is portalled into it. A node-less Dockable.Row inside renders that layout's root row, and panels of tabs in that layout are positioned in the window. Pop a tab out with tab.popout."

<Dockable.Root
    model={model}
    popoutTitle={(layout) => `My app (${layout.id})`}
    onPopoutOpen={(_layout, _window, doc) => {
        doc.documentElement.dataset.theme = document.documentElement.dataset.theme;
    }}
>
    {/* … */}
    <Dockable.Popout<Types> className="palette-surface bg-palette-base">
        {() => (
            <Dockable.Row<Types> renderSplitter={renderSplitter}>
                {renderNode}
            </Dockable.Row>
        )}
    </Dockable.Popout>
</Dockable.Root>

A window is state: model.get("windows") lists one WindowLayout<T> per popout (its id, its screen rect, its root row, its active and maximized tabsets). tab.popout and tabset.popout add one, window.close removes one (docking its tabs back into the main layout), and window.configure records its rect. A saved layout with windows reopens them when it loads.

model.run("tab.popout", { tabId: "chart-1" }); // a new window layout: the root opens its window
model.run("window.close", { windowId: "window-1" }); // its tabs dock back, the window closes

Props

PopoutProps<T> extends the common primitive props. The common props apply to each window's root element.

proptypedefaultdescription
children(layout: WindowLayout<T>) => ReactNoderequiredrenders a window layout, usually <Dockable.Row>{renderNode}</Dockable.Row>

The window itself is set up on Dockable.Root: popoutURL, supportsPopout, openWindow, popoutMirrorRoot, popoutTitle, onPopoutOpen, onPopoutClose.

State

PopoutState:

fieldtypedescription
layoutIdstringthe id of the window layout

Data attributes

attributevalue
data-layout-paththe window layout's path, e.g. /sublayout1

Its tabsets and tabs carry paths under it: /sublayout1/ts0/tb0. n in /sublayout<n> is the lowest number free when the window is first seen, kept while it is open (closing one window never renames another's elements). The element it is portalled into (created by the core in the popout document) carries data-dockable-popout (POPOUT_ATTRIBUTE), whose value is the window layout's id.

Structural style

position: absolute; inset: 0: it fills the window and is the containing block the window's panels are positioned in.

Behaviour

  • Renders nothing for a window until it has loaded and its styles are copied.
  • The core owns the windows: it opens one per window layout while the root is attached, and closes those whose layout left the state. Opening is idempotent per layout, and a release is deferred, so a React StrictMode remount keeps one window.
  • Closing a window from the browser runs window.close: its tabs dock back into the main layout's active tabset (its first tabset when none is active), and onPopoutClose is called first. The content of those tabs moves back to the main document before the window goes, so it keeps its state.
  • Without popout support (supportsPopout false), or when the window cannot open (openWindow returns null, a blocked window.open), the window layout docks back (window.close).
  • The window's screen rect is recorded with a transient window.configure when it is resized, and by a poll (a window that only moves fires no event), only when it changed.

The core: PopoutManager

The React part only portals. The windows belong to the core's PopoutManager<T>, one per main layout's engine, on the adapter's side (engine.adapter.getPopoutManager()), which an adapter for another framework uses the same way (an app never needs it):

memberdescription
setOptions(options: PopoutOptions<T>)the window options (below); the engine passes its popout option
attach() / detach()the main layout's engine was attached / detached: opens the windows of the state's window layouts / closes them after the current task, unless the engine is attached again
sync()opens a window for every window layout without one and closes those whose layout is gone (the engine calls it on every change)
subscribe(listener), getSnapshot()a window opened, became ready or closed
getContentRoot(layoutId)the element to render the window layout into, once its window is ready (undefined before)
getLayoutEngine(layoutId)the window layout's own LayoutEngine, created when its window opens
getWindow(layoutId), getOpenLayoutIds()a layout's native window; the layouts with an open window
getPopoutURL(), isSupportsPopout()the host page; whether window layouts open as popouts
close(layoutId)closes a layout's window now (the layout stays in the state)
dispose()closes every window and releases every resource

PopoutOptions<T>, the engine's popout option (Dockable.Root fills it from its props):

optiontypedescription
popoutURLstringthe popout host page; default "popout.html". The window layout's id is passed as ?id=
supportsPopoutbooleanwhether window layouts open as popouts at all; default: the main window has a fine hover pointer (a desktop)
title(layout: WindowLayout<T>) => string | undefinedthe popout document's title; with none, the host page's title is kept
onPopoutOpenPopoutCallback<T>the popout document is ready, before its content renders (e.g. to set up a css-in-js cache)
onPopoutClosePopoutCallback<T>the popout window is closing
mirrorRootboolean | readonly string[]copies the main document's <html> and <body> attributes into each popout and keeps them in sync
openWindowOpenWindowopens the native window (default: the main window's open)

OpenWindow is (url: string, name: string, features: string) => Window | null. The name is scoped to the layout (dockable-<scope><layoutId>), so two models on one page never share a window; features carries the window layout's rect (left, top, width, height). Inject it to open windows another way (an Electron BrowserWindow, a test double), or return null to refuse, which docks the window layout back.

PopoutCallback<T> is (layout: WindowLayout<T>, window: Window, document: Document) => void.

Style mirroring

A popout document gets the main document's styles, and keeps them in sync while it is open: the <link rel="stylesheet"> and <style> elements of the main document's <head> (added, removed, or with text edited in place), CSSOM rules inserted by css-in-js libraries (polled), and adopted (constructable) stylesheets, copied into one <style> element. A window renders once its linked stylesheets loaded, failed or timed out, so a blocked stylesheet never keeps it blank. <html> lang is always copied, and <html> dir is always the main layout's direction (it follows a flip); mirrorRoot copies more of <html> and <body>, never dir. A <style> or <link> placed in <body> is not copied: keep the styles a popout needs in <head>.

exportdescription
POPOUT_ATTRIBUTE"data-dockable-popout": the attribute of the element an adapter renders a window layout into

See the Popouts guide.