JsonViewer
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/json-viewer.jsonWhat to reach for when a product has to show a response, a settings file, a
payload — data the reader needs to understand, not edit. The alternative it
replaces is JSON.stringify(value, null, 2) in a <pre>, which is fine for
twenty lines and useless for two hundred: nothing folds, nothing is findable,
and the shape of the document is somewhere inside the indentation.
<JsonViewer value={payload} label={t('webhook.payload')} onActivate={(row) => copy(row.path)}/>It opens what it can show, not everything two levels down. A branch opens
on arrival when it is within two levels and holds at most twenty entries. The
size half is the half that matters: a 1204-entry array at the top level is
inside any depth bound worth having, and under depth alone this viewer rendered
1216 rows before the reader had touched it. branchPaths(value, { depth, size })
in json-rows is there when you want different bounds.
A closed branch says how many it holds, rather than previewing its contents: a preview of the first entries reads as though those are all of them.
A string is quoted and a number is not, and that is not decoration. "1"
and 1 are different values, and a number that arrived as a string is the
commonest bug in any payload. The quotes say so to every reader; the
--syntax-* colour says it faster to those who see
it. Never the colour alone.
null is drawn as the word. A key present and null is not a key missing,
and an empty cell says the second when the data says the first.
One tab stop, and the arrows move a cursor inside it. A document of four
hundred rows with a tabIndex on each is four hundred stops between whatever
is above it and whatever is below — the arrangement a radio group has, and for
the same reason.
| Key | |
|---|---|
| ↓ ↑ | The next and previous visible row |
| → | Open a closed branch; step into an open one; nothing on a leaf |
| ← | Close an open branch; otherwise go to the parent |
| Home End | The first and last row |
| Enter Space | Open or close, and report it |
→ does nothing on a leaf rather than falling through to the next row, which would make it a second ↓ and lose the reader their place in the nesting. ← on a leaf goes to the parent, which is how a reader gets out of a deep branch without walking back up through every sibling. Both come from the ARIA tree pattern, and both are what a hand-rolled tree leaves out after implementing Up and Down.
onActivate hands back the whole row, including its path — $.items[3].name
— which is what “copy this value” and “copy this path” hang on. Keys that dot
notation would break are bracketed, so the path resolves: $["user name"], and
$["a.b"] for the single key a.b, which written as $.a.b would silently
point at something else.
It is not TreeView with JSON poured in. A row here
is a key and a value, and TreeView’s row is one ReactNode label — with JSON
inside it, nothing can colour the value by type, tell an index from a name, or
offer the path. What the two genuinely share is the arithmetic of which rows
are visible, and that is shared:
json-rows is the sibling of
tree-rows.
Props
Read from the types in ui/json-viewer.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.
| Prop | Type | Default | |
|---|---|---|---|
valuerequired | JsonValue | The document to show: anything JSON.parse returns. | |
open | ReadonlySet<string> | Which branches are open, by path. Uncontrolled if omitted, starting with the top two levels open (a branch of twenty entries or more stays shut) - the shape of an answer rather than the whole document. | |
onOpenChange | (open: Set<string>) => void | Called with the new set of open paths whenever a branch is opened or closed. Pair it with open to control the viewer. | |
onActivate | (row: JsonRow) => void | Told which row was activated, with its path and its value. What a product hangs "copy this" or "go to this setting" on. | |
labelrequired | string | What a screen reader calls it. Required, and without a default: a string this component invents is a string the product cannot translate. | |
root | string | '$' | The path the document starts at, for a viewer showing one field of a larger record. Paths are then written so they still mean something in that record. |
className | string |
Accessibility
The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 19 rules applied and passed, and none failed. The accessibility report has every item.
Left to a person, where axe could not decide: color-contrast.
History
Each release that changed ui/json-viewer.tsx - the file a product copies - newest first.