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

Dockable.Tab

A draggable tab button that follows the APG tabs pattern with manual activation, its states computed from the model.

"A tab button (role="tab"), following the APG tabs pattern with manual activation: arrow keys (along the tab list's orientation, mirrored in RTL), Home and End move focus; click, Enter or Space selects. Enter or Space on the selected tab moves focus into its panel. Renders only its children."

<Dockable.Tab
    node={tab}
    className="px-3 data-selected:bg-palette-soft data-dragging:opacity-40"
>
    {tab.label}
    {model.can("tab.close", { tabId: tab.id }) ? <CloseButton tab={tab} /> : null}
</Dockable.Tab>

Render it from a Dockable.TabList child function. The tab is a plain TabOf<T> node: its text is the app's (tab.data), and what it may do is asked of the model (model.can, model.get("tab-settings-by", { tabId })), never read from the node.

Props

TabProps<T> extends the common primitive props.

proptypedefaultdescription
nodeTabOf<T>requiredthe tab to render
childrenReactNodenonethe tab button's content; the primitive renders no text of its own

State

TabState:

fieldtypedescription
selectedbooleanthe tab is its tabset's selected tab (model.is("tab-selected", { tabId }))
pinnedbooleanthe tab is pinned (tab.pinned === true)
draggingbooleanthe tab is being dragged
poppablebooleanthe tab can be popped out into a window: popouts are supported and model.can("tab.popout", { tabId }) accepts it now
overflowHiddenbooleanthe tab does not fit in its strip, so it is hidden (tab overflow)

Data attributes and ARIA

attributevalue
data-layout-paththe tab button path: /ts0/tb1
data-selectedthe selected tab
data-pinnedpinned
data-draggingbeing dragged
data-poppablepopouts are supported and the model accepts tab.popout for it (model.can: its resolved poppable, and every middleware)
data-overflow-hiddenthe tab does not fit in its strip, so it is hidden (tab overflow); it also gets display: none
idengine.get("tab-button-dom-id-by", { tabId: tab.id }), unique on the page (getTabButtonId(id, scope): dockable-<scope>tabbutton-<id>)
role"tab"
aria-selectedtrue or false
aria-controlsits panel's id (engine.get("tab-panel-dom-id-by", { tabId: tab.id }))
aria-keyshortcutsfocusTabToggle and, when its resolved closable is true and it is not pinned, closeTab
tabIndex0 on the tab stop (the selected tab, or the first tab when none is: engine.is("tab-tabbable", { tabId })), else -1
draggablethe tab's resolved draggable (model.get("tab-settings-by", { tabId })?.draggable)

Keyboard

keydoes
Left/Right (Up/Down when vertical)focus the previous/next tab
Home / Endfocus the first/last tab
Enter, Spaceselect; on the selected tab, move focus into its panel
closeTab (Ctrl+Delete)run tab.close when model.can accepts it, focusing a neighbour first
focusTabToggle (off by default)move focus into the tab's content

Behaviour

  • A click runs tab.select. In a border, a click on the selected tab closes the border's panel (border.configure with open: false).
  • Every command goes through model.run, so the model's middleware sees it and may veto it.
  • It is the drag source for its tab (useDragNode), and the element the browser snapshots as the drag image.
  • The engine measures it (for drop hit-testing within the strip).

Tab overflow

A tab that does not fit in its strip gets display: none (structural) and data-overflow-hidden. The arrow keys still reach it: moving onto a hidden tab selects it, which brings it into the strip.