Blog / Engineering
Sections that push, tabs that tear out
Our last dashboard post was about fluid
boards, a split layout and a drag grip, in @grafloria/element 0.4.9. The
kit is at 0.7 now, with a package of its own, @grafloria/dashboard. This is
what changed in between that you would notice in an app: sections that push, captions the
kit paints, tab containers you can tear apart, drops that mean what they show, and a few
fixes you only meet when they bite.
Sections are solid, and a growing section pushes
A section is a tile that holds tiles: a "Revenue" panel with three charts in it. The first rule we changed is that a section is solid. A widget dragged across the board no longer shoves a section out of its way the way it shoves a small tile; it is refused, and the section stays put. A section moves only when you mean it to: when you drag the section itself, or when something next to it grows.
Growing is where the old kit gave up. Pull a chart inside a section taller than the section's rows and the section grows to hold it. But until 0.7, if another section sat below, the growth stopped at it and the pull was capped. Now the growing section pushes everything below it down, other sections and tab containers included, and the whole thing is one undo step. The same is true from code:
const board = dashboard({ /* … */ });
// Past its section's rows: the section grows by the rows it needs, and pushes what is below it.
await board.widget('k-rev').resize(1, 2);
// One ⌘Z puts the widget, its section and everything that moved back.
A section that should never change size says so: with sizing: 'fit' the
section is the bound, and a child that needs a row it does not have is refused instead.
Captions the kit paints
A section can carry a caption band: a title, a subtitle, an icon, and buttons. You describe it as data, and the kit paints it, keeps it out of the way of its neighbours, and tells you when a button is pressed:
{ id: 'sales', widgets: [/* … */],
caption: {
text: 'Sales', subtitle: 'This quarter', icon: '📈',
position: 'inside', // or 'tab': a tab above the frame
show: 'always', // or 'design', 'hover'
actions: [{ id: 'export', label: 'Export' }],
} }
// options: onCaptionAction: (sectionId, actionId, viewId) => … handle.setCaption(id, caption)
The actions are real buttons, a press on one never selects the section, and the band
is a group labelled with its section's name for assistive technology. When the band is not
enough, renderCaption lets you paint your own.
Tab containers
A section with layout: 'tabs' shows one page at a time behind a tab strip.
The strip is a proper tablist: one tab stop, arrow keys to move, Home and End to jump, and
the arrows mirrored on right-to-left pages. From code, activateTab,
getActiveTab, moveToTab and moveTab drive it, and
onTabChange reports it.
Tabs can also be taken apart by hand, the way editor tabs can. Drag a tab out of its strip and the page becomes a section of its own where you drop it; a chip with the tab's label follows the pointer and a dashed placeholder shows where it will land. Drop it on another group's strip and it joins that group; on a split board, drop it beside a pane and it becomes a pane. Undo puts it back and shows the page that was showing before the drag, not whichever page happened to be first.
For read-only viewers there is tabs: { hidden: true }: no strip at all,
nothing reserved for it, pages switched from code. A designer leaves it off so the author
can reach every page.
Drops that mean what they show
On a board with sections inside sections inside tabs, "where will this land?" used to
have several answers depending on which code path asked. There is now one recursive
answer: the same resolver decides what the placeholder shows and what the drop does, down
to the deepest board the nesting option allows. A widget dropped on a
section's caption band means "into this section". A drop that will be refused shows its
cell in red under the pointer, so a refusal is visible before you let go. And a palette
drop through addWidget(spec, viewId, { displaced }) lands exactly on the cell
the preview showed, pushing the same tiles the preview pushed.
Fixes you only meet when they bite
⌘Z undid every board on the page. With two diagrams or dashboards on one page,
a single ⌘Z undid a step in each of them, and one Delete deleted in each. Keys now act on
the canvas you last pressed or focused. If your own Undo button calls
getEngine().commandManager, nothing changes for it.
Disposing the diagram left the board behind. Disposing the render instance did not dispose the dashboard handle on it, so the old board's tools kept taking presses on the next board mounted in the same element. React's StrictMode, which mounts everything twice in development, made this easy to hit. Disposing the diagram now disposes its board too; calling both is still fine.
A saved board carried functions. toJSON() returns plain data
now: no callbacks, nothing that cannot go through JSON.stringify and come
back.
Split views forgot your sizing. A split view always lays out to fit its frame,
but switching a view to split and back used to leave it in fit mode. The board now keeps
the sizing you chose, the split view draws fit anyway, and switching back to grid gives
you your setting again, saved in toJSON.
What we got wrong
The section that stopped growing was found by a review of our documentation, not by
our checks, because every check grew a section with nothing below it. The scenario that now
covers it puts a second section directly underneath and pulls a widget past the first
one's rows, and it fails without the fix. While fixing it we found that
resize() from code still refused where a hand pull grew: two paths to the same
edit had drifted apart. Both now grow the section the same way, and each has its own
scenario.
Get it
Install @grafloria/dashboard, or keep using @grafloria/element,
which it re-exports. Everything above is in 0.7, MIT. The
fluid dashboard has tabs, captions,
splits and sizing in its toolbar, the
nested containers demo has
sections in sections, and the dashboard
builder shows palette drops. For a board with stronger opinions, read
A Kanban board is a dashboard with
opinions.