Lattice Grid Buy a licence

blog

Context menu, sidebar and toolbar, 1.108

Building…
Loading a live grid…

Every data app ends up with the same three pieces around its grid: a menu that opens on right-click, a navigation rail down the side and a toolbar across the top. Each one gets hand-built, usually by a different person in a different week, and none of them match the grid. The menu has its own padding, the rail ignores dark mode, and the toolbar’s Export button knows nothing about the rows the user has filtered.

The example above is a small app shell with all three, built on one grid. Choose a page in the sidebar and the grid changes to it. Type in the filter box and the rows narrow. Right-click a row and the menu opens. Press Dark, then Narrow, and watch the toolbar fold its trailing buttons into a more button that opens the same menu. This is 1.108, and the three pieces share a theme, a keyboard model and one menu, so the app looks like one thing.

A context menu you declare once

createContextMenu takes the items you declare and opens them wherever you point it. openContextMenu is the trigger helper: hand it a right-click event, a point or an element, and it opens the menu inside the viewport and stops the browser’s own menu opening over it.

import { createContextMenu, openContextMenu } from '@toclocoinc/lattice-grid/modules/contextmenu';

const menu = createContextMenu({ items: [
  { name: 'Rename', shortcut: 'F2', action: (ctx) => rename(ctx.item) },
  { name: 'Share', children: [{ name: 'Copy link', action: copyLink }] },
  'separator',
  { name: 'Archived', checked: true, action: toggleArchived },
] });

menu.show({ x: 40, y: 40 });            // at a point
openContextMenu(event, { items, ctx: { row } });  // at a right-click

An item can nest a submenu with children, carry a check mark with checked, show a row of colour swatches with swatches, or be a function that decides its own shape from the row that was clicked. Every menu is a real role="menu", with arrow keys, Enter, Escape, Home, End and type-ahead, and focus returns to what opened it.

The part that makes the three pieces match: the grid’s own cell and column-header menu now open through this module, and so do the item menus of the tree and the list view. Right-click a row in the example and you are using the same menu code that the toolbar’s more button opens. Your own menus and the grid’s look and behave the same because they are one menu.

A navigation sidebar that calls your router

createSidebar draws a navigation tree: groups, nested branches, badges and an active page. A click on a page goes through your onNavigate first, then the page is selected, and the current page is marked for screen readers with aria-current="page". You keep your router; the sidebar only tells you where the user wants to go.

const sidebar = createSidebar(el, {
  items: [
    { id: 'open', text: 'Open', count: 14 },
    { id: 'reports', text: 'Reports', nodes: [{ id: 'by-region', text: 'By region' }] },
  ],
  active: 'open',
  search: true,
  railToggle: true,
  onNavigate: ({ id }) => router.go(id),
});

search: true adds a filter box that shows matching pages and the groups needed to reach them. railToggle: true adds a button that collapses the rail to icons only, and a hovered icon shows its label. Try both in the example: type “ship” into the sidebar’s filter, then collapse the rail.

A toolbar bound to the grid

createToolbar draws buttons, toggles, dropdowns, inputs, titles and your own markup. Pass it a grid with bind and give an item an action, and the toolbar drives the grid for you: export, undo, redo, addRow, removeRow, expandAll, collapseAll, submit and refresh. Items disabled by the view’s own state, such as Undo with nothing to undo, follow it without any code from you. The same binding works for a tree, a list view and a form.

const toolbar = createToolbar(el, {
  bind: grid,
  items: [
    { id: 'add', value: 'Add', action: 'addRow' },
    { id: 'export', value: 'Export', action: 'export' },
    { type: 'spacer' },
    { id: 'filter', type: 'input', placeholder: 'Filter orders', overflow: 'never' },
  ],
});
toolbar.on('input', ({ value }) => grid.filters.quick(value, { mode: 'contains' }));

When the row is too narrow, the trailing items move into a more button that opens the context menu. Mark an item overflow: 'never', as the filter box is here, and it stays put while the buttons fold. Press Narrow in the example to see it.

All three follow the grid themes, the design-system presets and dark mode, work from the keyboard, and are announced correctly by screen readers. The menu, sidebar and toolbar reference lists every option.

Upgrading from 1.107

There is nothing to change. The grid’s built-in cell and column-header menu now renders through the context menu module, with the same items and the same keys. The sidebar and toolbar are new surface and opt in by use.

The changelog has the full list. The grid engine page covers the core all three sit around. Open the example, right-click a row, and then narrow the toolbar.

Read next

All posts RSS