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.
| prop | type | default | description |
|---|---|---|---|
node | TabOf<T> | required | the tab to render |
children | ReactNode | none | the tab button's content; the primitive renders no text of its own |
State
TabState:
| field | type | description |
|---|---|---|
selected | boolean | the tab is its tabset's selected tab (model.is("tab-selected", { tabId })) |
pinned | boolean | the tab is pinned (tab.pinned === true) |
dragging | boolean | the tab is being dragged |
poppable | boolean | the tab can be popped out into a window: popouts are supported and model.can("tab.popout", { tabId }) accepts it now |
overflowHidden | boolean | the tab does not fit in its strip, so it is hidden (tab overflow) |
Data attributes and ARIA
| attribute | value |
|---|---|
data-layout-path | the tab button path: /ts0/tb1 |
data-selected | the selected tab |
data-pinned | pinned |
data-dragging | being dragged |
data-poppable | popouts are supported and the model accepts tab.popout for it (model.can: its resolved poppable, and every middleware) |
data-overflow-hidden | the tab does not fit in its strip, so it is hidden (tab overflow); it also gets display: none |
id | engine.get("tab-button-dom-id-by", { tabId: tab.id }), unique on the page (getTabButtonId(id, scope): dockable-<scope>tabbutton-<id>) |
role | "tab" |
aria-selected | true or false |
aria-controls | its panel's id (engine.get("tab-panel-dom-id-by", { tabId: tab.id })) |
aria-keyshortcuts | focusTabToggle and, when its resolved closable is true and it is not pinned, closeTab |
tabIndex | 0 on the tab stop (the selected tab, or the first tab when none is: engine.is("tab-tabbable", { tabId })), else -1 |
draggable | the tab's resolved draggable (model.get("tab-settings-by", { tabId })?.draggable) |
Keyboard
| key | does |
|---|---|
| Left/Right (Up/Down when vertical) | focus the previous/next tab |
| Home / End | focus the first/last tab |
| Enter, Space | select; 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.configurewithopen: 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.