developer guide
Cell Comments in a JavaScript Data Grid
A comment belongs to a cell, so the conversation about a number sits on the number. Threads resolve, the unresolved ones are counted, and the grid can filter to the rows people are actually discussing.
Developer guide › Collaboration and comments › Cell Comments in a JavaScript Data Grid
Cell comments
Threaded discussion attached to individual cells, for reviewing data with other people without leaving the grid.
Wiring a provider
comments: {
provider: {
loadIndex: (rowIds, fields) => api.counts(rowIds, fields),
loadThread: (cellKey) => api.thread(cellKey),
addComment: (cellKey, body, parentId, ctx) => api.add(cellKey, body, parentId, ctx.value),
editComment: api.edit, deleteComment: api.remove,
resolveThread: api.resolve, unresolveThread: api.unresolve
},
rowLabel: (row) => row.data.name
}
Comments are not row data, and never enter the column store. They have a different lifecycle: append-mostly, sparse against the row count, carrying their own identity and timestamps, subject to their own permissions, and outliving the values they annotate. Putting them in the store would mean columnarising a field that is empty for almost every row and rebuilding it on every write.
Two levels of data, and only one of them is cheap. The index maps a cell
to { count, unresolved, updated } and nothing else; it is consulted on every
repaint, so it must never carry bodies. A thread is the bodies for one cell, loaded when the
thread opens and dropped when it closes. Holding every thread a user opened would accumulate
the whole comment dataset over a long session to render a few triangles.
Stable row identity is a hard requirement, enforced rather than documented.
A comment is keyed on row identity plus field. Row index changes under sort, filter and
grouping, so a comment keyed on it reattaches to whichever row later occupies that position,
and a comment on the wrong row is worse than no comment at all. A grid with no
rowKey disables comments and names them in the same single warning as the other
identity-dependent features.
Identity has to survive more than the session. Comments outlive the page that wrote them, so a key that is stable only within one load (anything derived from arrival order, for instance) is not enough. Reload the data in a different order and every thread points somewhere else. If you have only used the grid client-side you may never have needed a durable identity before; you do now.
Writes are optimistic, and rejections are taken back. The author sees their own comment at once, faded until the provider confirms. A rejection removes it rather than leaving the grid displaying something the server refused. The same pattern as cell editing, for the same reason.
The grid authorises nothing. A comment may carry
can: { edit, delete, resolve } and the affordances follow it, but that is a
convenience for the person looking at the screen. It is not a control, and the documentation
says so here rather than leaving it to be assumed: hiding a button stops nobody who opens the
console. Reject in the provider.
A body is user input that has round-tripped through your storage. That is
the exact shape of a stored cross-site script, so the default path sets text and nothing else.
Turning on markdown buys emphasis, code and links: three constructs, built
as elements rather than parsed as markup, with any scheme other than http,
https and mailto refused. A refused link still shows its label, so
nothing the author wrote vanishes without trace.
The marker cannot move the cell's contents. It is a corner triangle drawn with a border on a pseudo-element, so it occupies no space in the layout: no shifted text, no rewrapped number, no displaced sparkline. That constraint is why it is a corner rather than a badge: every other position in a cell is already spoken for. Only the corner opens a thread; a click elsewhere belongs to selection, and taking it would make commented cells behave unlike every other cell. The hit region is larger than the drawn mark, because at compact density the triangle is about seven pixels across.
A comment shows the value it was written against whenever that no longer matches the cell. Without it, a note reading "this looks too high" sits beside a number it never described and the reader concludes the comment is wrong. Changing a value never deletes or invalidates a comment.
Filtered-out comments are hidden, not lost, and the grid says so.
hiddenUnresolved() reports what is still outstanding on rows the filter is
hiding, because a user who filters and sees no markers should not conclude there is nothing
left to deal with. The status bar carries this: its comments panel reads
“3 unresolved comments on hidden rows” and is silent whenever the count is zero, so
a grid with nothing outstanding gains no permanent furniture. The panel is in the default set,
and can be placed explicitly like any other:
statusBar: { panels: ['rowCount', 'comments'] }
The count returns zero rather than a number it cannot stand behind: it is only meaningful
once the index covers every row, so it stays at zero, and the panel stays silent,
until loadAll() has resolved.
The comments-only filter is refused rather than approximated. Restricting
the grid to rows carrying comments needs the index to cover the whole row set, not just what
has been scrolled past, a partial answer would hide precisely the rows the user opened
it to find. Call loadAll() first; until complete is true,
filterToCommented() returns false and does nothing.
Comments stay available while streaming, unlike header histograms: a comment does not move when new rows arrive. Index loads for new rows follow the same debounced viewport path. A thread whose row is evicted by a bounded window closes with a short explanation rather than hovering over a row that has gone.
Keyboard and screen reader. Alt+M opens the thread
on the focused cell: Alt because the grid binds nearly every unmodified key to
navigation and editing. The panel traps Tab, which it has to: the grid behind it is
still there and still focusable, so without the trap a keyboard user would be moving through
cells with a dialog open over them. Focus returns to the originating cell on close rather than
being dropped at the top of the document. Cells announce their comment count and unresolved
count through aria-description rather than their label, because the label is the
cell's value and burying a count inside it would make every commented cell read as something
other than what it holds.
Outside its scope: mentions, notifications, rich text, attachments,
reactions, row-level and column-level comments, and export of comments. The grid opens no
transport of its own: if your application pushes updates, call refresh()
and the index reloads.