Lattice Grid

blog

Know when the grid is finished: one attribute for your tests and your page

Building…
Loading a live grid…

A test sorts a column and reads the first cell. Is that cell the new first row or the old one? After a filter, a scroll, or a page of rows arriving from a server, the question is the same: when is it safe to look? Until now a test had to guess. Wait for the mount. Then wait for a loading marker. Then wait for the DOM to stop changing for a couple of frames, and hope the frames were enough.

The grid above holds 200,000 rows generated in the tab. Sort a column, open a header filter or type in the quick filter box, and watch the badge beside it. It reads “working” while the grid has something outstanding and “settled” once it has not, with the time of the last change. Then press “run a check”. It does what a test does: clicks a header, waits, and reads the first cell and the row count.

The grid says so itself

The element with role="grid" carries aria-busy. It reads "true" from the moment any of this is outstanding:

  • a scheduled render frame
  • a rows update
  • a sort, a filter or a quick filter
  • a scroll that schedules a frame
  • a remote or paged request
  • an edit commit waiting on its handler or the source

It reads "false" only once the last frame of that work has painted and nothing further is queued. On mount it reads "true" until the first render has painted. There is no window in which the rows are still changing because of that work while the attribute says "false".

The wait is one attribute

The check button in the example is the whole idea. After the click, it waits for the attribute and reads:

const root = document.querySelector('[role="grid"]');
header.click();
await settled(root);                       // resolves when aria-busy is "false"
const first = root.querySelector('[role="gridcell"][data-col="amount"]');
console.log(first.textContent, root.getAttribute('aria-rowcount'));

The badge uses the same attribute, through a MutationObserver. Nothing in it is a timer. The same few lines work in Playwright, in Cypress, in a plain script, or in your own page when it wants to show a spinner.

Every view, one rule

The grid, the list and data views, Kanban, Gantt and the calendar all carry the attribute, from the same rule. It is also the signal a screen reader already understands, so a busy grid is announced as busy and the assistive technology is not left reading a half-updated table.

Two companions

Range selection can be read back. Every cell inside a range selection carries aria-selected while it is selected, whether the range came from a pointer drag or from Shift with the arrow keys. A cell outside the range carries no aria-selected at all. A test selects a range and reads it back from the page.

Dialogs can be found. The header filter dialog and the column menu carry data-col, naming the column, and an id. While one is open, the header control that opened it points at it with aria-controls. A test no longer has to assume the newest dialog on the page is the right one.

Counts over a database

One more change, for grids over a database. A count tile beside a grid over a database, paged or remote source now shows the matching total of the whole relation, rather than the length of the page that happens to be loaded. Sum, avg, min and max tiles ask the engine for the figure.

Open the example, sort a column and watch the badge, then run the check. The changelog has the rest.

Read next

All posts RSS