api reference
Menu, sidebar and toolbar: JavaScript Data Grid API
createContextMenu and openContextMenu, createSidebar, and createToolbar bound to the actions of a view.
API reference › Menu, sidebar and toolbar
The context menu module
modules/contextmenu is the one menu every Lattice surface opens: the grid's own cell and column-header menu renders through it, and the tree and list view open it from their itemContextMenu event, so the grid, the tree and the list all share one keyboard-accessible menu and one theme. createContextMenu is the factory - declare the menu's items up front (createContextMenu({ items, ctx, onClose, host })) or pass a host element first (createContextMenu(el, { ctx })), then menu.show({ x, y }) opens it (or anchor/placement on an element) and menu.hide() closes it. An item is an object - name, icon, shortcut, disabled, checked, children (a nested submenu), swatches (a colour-swatch row whose action(ctx, colour) receives the picked colour), action(ctx) - or the string 'separator' for a divider, or a function (ctx) => item | string | null | false so a host decides the shape at open time from the cell or item that was right-clicked; resolveMenuItems is that same resolution as a pure helper. openContextMenu(trigger, config) is the trigger helper: hand it a contextmenu event (whose default it claims, so the browser's own menu does not open over it), a { clientX, clientY } point, or an element to anchor below, and it opens the menu inside the viewport now. Every menu is a real role="menu" of menuitem/menuitemcheckbox rows with aria-checked/aria-disabled, arrows, Enter, Escape, Home/End and type-ahead, and focus returns to the trigger on close. The class names are the grid's own lat-menu__*, so a menu opened beside a themed grid picks up that grid's theme with no second stylesheet.
import { createContextMenu, openContextMenu, resolveMenuItems } from '@toclocoinc/lattice-grid/modules/contextmenu';
const menu = createContextMenu({ items: [
{ name: 'Rename', action: (ctx) => rename(ctx.item) },
'separator',
{ name: 'Share', children: [{ name: 'Copy link', action: () => {} }] },
] });
menu.show({ x: 40, y: 40 }); // at a point - or { anchor: el, placement: 'below' }
menu.destroy();
openContextMenu(event, { items, ctx: { row }, returnFocus: event.target });
Executed: the item model, the factory, and the trigger helper.
const { createTestDom, TestEvent } = await import('../packages/dom/src/renderer/testdom.js');
const { createContextMenu, openContextMenu, resolveMenuItems } = await import('../packages/modules/contextmenu/index.js');
const { document, root } = createTestDom();
// resolveMenuItems: functions run against the context, a bare string is a
// named entry, 'separator' is a divider, children nest, and a divider left
// trailing by a filtered-out entry is dropped.
const resolved = resolveMenuItems([
(ctx) => ({ name: `Rename ${ctx.row}` }),
'separator',
'Plain',
{ name: 'Share', children: [{ name: 'Copy link', action() {} }] },
{ name: 'Mute', disabled: true, checked: true, icon: 'close', shortcut: 'Ctrl+M' },
'separator',
() => null,
], { row: 'A1' });
const shape = resolved.map((i) => i.separator ? '---' : (i.subMenu ? 'Share/Copy link' : i.name)).join(',');
const mute = `${resolved[4].disabled},${resolved[4].checked}`;
// createContextMenu({ items, ... }) - the factory shape - opens at a point.
const menu = createContextMenu({ document, host: root, items: [
{ name: 'Cut', icon: 'pencil', shortcut: 'Ctrl+X' },
{ name: 'Copy', checked: true, icon: 'copy' },
'separator',
{ name: 'Paste', disabled: true, icon: 'download' },
] });
const el = menu.show({ x: 30, y: 20 });
const labels = [...el.querySelectorAll('.lat-menu__item__label')].map((n) => n.textContent).join(',');
const checked = el.querySelector('[role="menuitemcheckbox"]').getAttribute('aria-checked');
const disabled = el.querySelector('[aria-disabled="true"]').querySelector('.lat-menu__item__label').textContent;
const role = el.getAttribute('role');
menu.hide();
// openContextMenu claims a right-click so the browser's own menu does not open
// over it, then opens at the pointer.
const ev = new TestEvent('contextmenu', { clientX: 40, clientY: 30 });
const opened = openContextMenu(ev, { host: root, items: [{ name: 'Inspect' }] });
const pointerLabel = opened.root.querySelector('.lat-menu__item__label').textContent;
const claimed = ev.defaultPrevented;
opened.hide();
// And it anchors below an element when handed one.
const anchor = document.createElement('button');
document.body.appendChild(anchor);
const anchored = openContextMenu(anchor, { host: root, items: [{ name: 'Anchor' }] });
const anchoredOk = !!anchored.root;
anchored.destroy();
return [shape, mute, labels, checked, disabled, role, pointerLabel, claimed, anchoredOk].join(' | ');
The sidebar module
modules/sidebar is an opt-in navigation rail - a tree of groups that
collapse, nested items, badges and an active item, with a filter box and an icons-only rail that
expands on demand - drawn by createSidebar(el, config), which returns a
Sidebar instance. A leaf is a destination: clicking it (or pressing Enter on it)
calls the host's onNavigate({ id, node, originalEvent }) and raises
navigate, then selects it. The host routes through that callback and tells the rail
what is now current with setActive(id) (or the active config key), which
paints the row aria-current="page". The rail itself is a navigation
landmark, so a screen reader can jump to it.
import { createSidebar } from '@toclocoinc/lattice-grid/modules/sidebar';
const sidebar = createSidebar(document.querySelector('#nav'), {
items: [
{ id: 'home', text: 'Home', icon: 'user' },
{ id: 'reports', text: 'Reports', nodes: [
{ id: 'sales', text: 'Sales', count: 3 },
{ id: 'ops', text: 'Ops' },
] },
],
active: 'home',
onNavigate: ({ id }) => router.go(id),
search: true,
railToggle: true,
});
The node tree. items (or its alias nodes) holds
the tree: a branch is any node with a nodes array, a group header is a node with
group: true, a badge is count (with countColor choosing the
role), and type: 'separator' draws a divider. A branch expands and collapses on
click, by expand/collapse/toggle, or with the arrow keys;
select(id) reveals a node by opening its ancestors.
Filtering is a class toggle, not a re-render. filter(term) shows
every node whose text matches and the ancestors needed to reach it, opening those ancestors, and
returns the match count; an empty term restores the tree. The optional search: true
draws the filter box and runs filter on each keystroke. Every node keeps its element,
so filtering never rebuilds the tree.
The icons-only rail. flat: true (or goFlat() to
toggle) collapses the rail to icons; a hovered or focused icon shows its label through the
grid's one shared tooltip, and the optional railToggle: true draws the button that
flips the rail, an aria-expanded toggle.
| Member | Description |
|---|---|
| createSidebar(el, config) | Create the rail; config takes items/nodes, active, onNavigate, search, railToggle, flat, ariaLabel and messages. |
| setActive(id) / select(id) / unselect() | Mark the page the host is showing (aria-current="page") and select it; select a node, revealing its ancestors; clear the selection. |
| expand(id) / collapse(id) / toggle(id) / expandAll() / collapseAll() / expandedIds() | Open, close or flip a branch, open or close all, and read the ids of every open branch. |
| filter(term, restore?) | Filter by text, case-insensitive; returns the match count. An empty term restores the tree and reopens restore. |
| goFlat(on?) | Narrow to icons only, or back; omit on to toggle. Returns whether it is now flat. |
| show(id) / hide(id) / get(id) / nodeElement(id) / parentsOf(id) | Show or hide a node; read the live node, its row element, or its ancestors' ids. |
| add / insert / remove / setNodes / data.* | Change the tree; data is the flat, id-keyed store (parse, add, update, remove, getItem, serialize, …). |
| on(name, fn) | Events: click, navigate, select, expand, collapse, flat, mouseEnter, mouseLeave, and the cancellable beforeExpand/beforeCollapse. |
| destroy() | Remove the DOM and listeners. The host still owns the element. |
| FALLBACK_EN | The frozen English strings the sidebar uses when no locale catalogue supplies a message, keyed by message key (sidebar.nav, sidebar.search, …). |
The tree, the filter, the rail and the active key, executed
A two-branch rail with an active item and a host onNavigate,
filtered to one leaf (opening its ancestor), narrowed to icons only and back, then re-pointed at a
different active node - run headless with the suite's DOM test double, since this module
needs a real element to mount into.
const { createTestDom, flushFrames } = await import('../packages/dom/src/renderer/testdom.js');
const { createSidebar, Sidebar, FALLBACK_EN } = await import('../packages/modules/sidebar/index.js');
const { document, root } = createTestDom({ width: 240, height: 400 });
globalThis.document = document;
const navigated = [];
const sidebar = createSidebar(root, {
items: [
{ id: 'home', text: 'Home', icon: 'user', count: 3 },
{ id: 'reports', text: 'Reports', nodes: [
{ id: 'sales', text: 'Sales' },
{ id: 'ops', text: 'Ops' },
] },
],
active: 'home',
onNavigate: ({ id }) => navigated.push(id),
});
flushFrames();
const isSidebar = sidebar instanceof Sidebar;
const activeCurrent = sidebar.nodeElement('home').getAttribute('aria-current');
const landmark = sidebar.el.getAttribute('role');
const matched = sidebar.filter('sales');
const salesVisible = !sidebar.get('sales').hidden;
const homeHidden = sidebar.get('home').hidden;
const reportsOpen = sidebar.get('reports').expanded;
const flat = sidebar.goFlat(true);
sidebar.filter('');
sidebar.setActive('ops');
const opsCurrent = sidebar.nodeElement('ops').getAttribute('aria-current');
sidebar.destroy();
return [isSidebar, activeCurrent, landmark, matched, salesVisible, homeHidden, reportsOpen, flat, opsCurrent, FALLBACK_EN['sidebar.nav']].join('|');
The toolbar module
modules/toolbar is an opt-in data-driven toolbar - a row of buttons,
two-state toggles, dropdowns, separators, spacers, inputs, titles and custom HTML - drawn by
createToolbar(el, config), which returns a Toolbar instance with a
data store (parse, add, update,
remove, getItem, serialize, …) so the bar can be
described once and changed item by item. When the row is too narrow for its items, the trailing
ones move into a “more” button that opens the context-menu module. An item bound to a
view names the view's action with action; the toolbar drives it on click and keeps
the item's disabled (and, for a toggle, its aria-pressed) following the
view's own state.
import { createToolbar } from '@toclocoinc/lattice-grid/modules/toolbar';
const tb = createToolbar(document.querySelector('#bar'), {
bind: grid,
items: [
{ id: 'undo', value: 'Undo', icon: 'undo', action: 'undo' },
{ id: 'redo', value: 'Redo', icon: 'redo', action: 'redo' },
{ type: 'separator' },
{ id: 'export', value: 'Export', icon: 'download', action: 'export' },
{ type: 'spacer' },
{ id: 'search', type: 'input', placeholder: 'Search', icon: 'search' },
],
});
tb.on('click', ({ id, item }) => { /* payload handlers */ });
tb.data.update('undo', { disabled: true });
The item model. items is the spec: id,
type (button, selectButton, title, separator, spacer, input, customHTML),
value (the label), icon from the grid's registry, tooltip,
hidden, disabled, count/countColor (a badge),
twoState/group (toggles, active state,
aria-pressed), items (a dropdown), placeholder and
width (an input), and overflow: 'never' to keep an item out of the
overflow menu. Any other property is carried through and returned from getItem and
serialize.
Binding to a view. bind names a grid, tree, list view or form;
an item's action (export, undo, redo,
addRow, removeRow, expandAll, collapseAll,
submit, refresh) is resolved duck-typed - undo runs
view.history.undo() on a grid and view.undo() on a form - and the
item's disabled follows the view's canUndo/canRedo (or the
method's presence) on refresh(). The binding owns those two properties on
action items; the host can also call toolbar.bind(view) and
toolbar.refresh() directly.
Overflow and keyboard. overflow: 'menu' (the default) moves the
trailing items into a “more” button that opens the context-menu module;
'scroll' scrolls and 'none' clips. The bar is a roving
role="toolbar": one tab stop, arrows move between items, Home/End jump the ends, and
a toggle carries aria-pressed. The overflow button's name reads the shipped catalogue
(toolbar.more).
| Member | Description |
|---|---|
| createToolbar(el, config) | Create the bar; config takes items, bind, overflow, moreIcon/moreLabel, tooltips, ariaLabel and messages. |
| bind(view) / bound / refresh() | Bind the bar to a view whose actions the action items drive; read the bound view; re-read every bound item's disabled/active from the view. |
| data.* | The item store: parse, add, update, remove, removeAll, getItem, exists, getIndex, getId, getItems, getParent, getLength, forEach, map, find, serialize. |
| show(id) / hide(id) / enable(id) / disable(id) / isDisabled(id) | Show, hide, enable or disable items (omitting id applies to the root items); read an item's disabled state. |
| select(id) / unselect(id?) / isSelected(id) / getState() / setState() | Toggle items and groups; read or write the stateful items' state in one object. |
| setFocus(id) / itemElement(id) | Focus an item's control; read an item's wrapper element. |
| on(name, fn) | Events: click, input, inputChange, inputFocus, inputBlur, change, keydown, openMenu. |
| destroy() | Remove the DOM and listeners. The host still owns the element. |
| FALLBACK_EN | The frozen English strings the toolbar uses when no locale catalogue supplies a message, keyed by message key (toolbar.more). |
The bar, the binding, the toggle and the overflow button, executed
A toolbar bound to a duck-typed view, run headless with the suite's DOM
test double: the undo action runs the view's history and disables once the timeline
is empty, a two-state button reflects aria-pressed, and the overflow button is named
from the catalogue.
const { createTestDom, flushFrames, TestEvent } = await import('../packages/dom/src/renderer/testdom.js');
const { createToolbar, Toolbar, FALLBACK_EN } = await import('../packages/modules/toolbar/index.js');
const { document, root } = createTestDom({ width: 640, height: 48 });
globalThis.document = document;
const undoLog = [];
const view = {
history: { canUndo: () => undoLog.length < 1, undo: () => undoLog.push('undo') },
expandAll: () => {}, collapseAll: () => {},
};
const tb = createToolbar(root, {
bind: view,
items: [
{ id: 'undo', value: 'Undo', icon: 'undo', action: 'undo' },
{ id: 'bold', value: 'Bold', twoState: true },
],
});
flushFrames();
const isToolbar = tb instanceof Toolbar;
const role = tb.el.getAttribute('role');
const undoEnabledAtStart = tb.isDisabled('undo') === false;
const click = (id) => tb.itemElement(id).querySelector('button').dispatchEvent(new TestEvent('click', { bubbles: true }));
click('undo');
const undoRan = undoLog.length === 1;
const undoNowDisabled = tb.isDisabled('undo') === true;
click('bold');
const pressed = tb.itemElement('bold').querySelector('button').getAttribute('aria-pressed');
const more = tb.el.querySelector('.lat-toolbar__more').getAttribute('aria-label');
tb.destroy();
return [isToolbar, role, undoEnabledAtStart, undoRan, undoNowDisabled, pressed, more, FALLBACK_EN['toolbar.more']].join('|');