Ready for Dev, so its markup, attributes, events and styles can all still change without notice,
it is not covered by the accessibility gate, and it is not published to npm. It is on this site so the work
can be seen and reviewed - nothing more.
TreeView
A hierarchical tree view component for navigating nested content.
Usage
-
src
- index.js
- app.js
- package.json
-
src
- index.js
- app.js
- package.json
<section is="root-tree-view" aria-label="File browser">
<ul role="tree">
<li role="treeitem" aria-expanded="true">
<span>src</span>
<ul role="group">
<li role="treeitem">index.js</li>
<li role="treeitem">app.js</li>
</ul>
</li>
<li role="treeitem">package.json</li>
</ul>
</section>
Which rows can be opened
A treeitem with aria-expanded is a branch and draws a chevron at the end of its
row; one without it is a leaf and draws nothing. The value the markup arrives with is the state the tree
opens in - so a page can render its own branch open and the rest closed, which is what this site's sidebar
does, without a script running after the first paint to do it as a visible jump.
A branch's children live in a <ul role="group">, and a closed branch has
hidden on it. Anything the page styles as a list inside a tree has to leave that attribute
room to work: a display declaration on the group outranks the
display: none the attribute carries, and the branch then collapses in the DOM while staying
on screen.
What opens a branch
The branch's own row - its direct > span - and nothing else inside it. That row is the
name, whatever sits beside it, and the chevron at the end of it; everything else in the
<li>
is the group the branch labels. A group is an indented column, so its inline gutter and the gaps between
its rows are its own box, and a click landing on any of them used to find the branch through
closest and close it: aiming at an entry and missing it by a few pixels collapsed the branch
holding it.
So a row's own content stays clickable for its own reasons - a link navigates, a leaf's row does nothing
- and only the branch row toggles. From the keyboard it is Enter, Space
and the arrows, on whichever row has focus.
A count at the end of a branch row
A branch row is a flex line: the name, whatever else the markup puts in it, and the chevron at the
trailing edge. Anything marked [data-part="count"] goes to that trailing edge too,
just before the chevron - auto on the inline-start margin, not
justify-content: space-between, which would spread all three and leave the count in the
middle of the row.
A root-badge is what it is usually made of, and its height is remapped from
--root-tree-view--branch--count--block-size
so the row decides how tall the row is: the badge's own small step is 25px, drawn for a
badge beside a heading, and at 25px it was the tallest thing in the row.
-
src3
- index.js
- app.js
- router.js
-
src3
- index.js
- app.js
- router.js
<section is="root-tree-view" aria-label="Changes">
<ul role="tree">
<li role="treeitem" aria-expanded="true">
<span>src<span is="root-badge" role="note" data-color="neutral" data-size="small" data-part="count">3</span></span>
<ul role="group">
<li role="treeitem">index.js</li>
<li role="treeitem">app.js</li>
<li role="treeitem">router.js</li>
</ul>
</li>
</ul>
</section>
Keeping the state across pages
persist
names where the tree writes which of its branches are open, under
root:tree-view:<name>
in localStorage. Without it the tree keeps nothing and opens in whatever state the markup
arrives in, every time.
It is what makes collapsing worth offering on a multi-page site: every link in a tree of links is a real
document load, so a reader who folds a long branch away to get at the one under it had it unfold again
on the next page. The state is applied before the first paint, so the tree is painted once - restoring
after it would be the visible jump that writing
aria-expanded
in the markup exists to avoid.
-
src
- index.js
-
docs
- readme.md
-
src
- index.js
-
docs
- readme.md
<section is="root-tree-view" persist="file-browser" aria-label="File browser">
<ul role="tree">
<li role="treeitem" aria-expanded="true" id="tree-src">
<span>src</span>
<ul role="group">
<li role="treeitem">index.js</li>
</ul>
</li>
<li role="treeitem" aria-expanded="true" id="tree-docs">
<span>docs</span>
<ul role="group">
<li role="treeitem">readme.md</li>
</ul>
</li>
</ul>
</section>
A branch is remembered by its id when it has one and by its position in the tree when it
does not. Give the branches ids if the tree can grow: a positional key is stable only while the shape
is, and a branch inserted above another hands that other one somebody else's state. It is never the
row's text - a sidebar heading that carries a count of what is under it is renamed the day something is
added to it, and every reader's state goes with it.
Storage is read and written inside try: a sandboxed document throws on the first touch of
localStorage, and a tree must not fail to render over a preference it was not allowed to
read.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
persist |
string |
- | Keep which branches are open, under root:tree-view:<name> |
data-current (on a treeitem) |
boolean | - | Marks the branch holding whatever the reader is on |
aria-label |
string |
- | Accessible label |
Parts
| Part | Description |
|---|---|
[data-part="count"] |
Trailing content of a branch row, pushed to the far end of it |
Events
| Event | Detail | Description |
|---|---|---|
root-tree-view.select |
{ value } |
Fired on item selection |
root-tree-view.expand |
{ value } |
Fired on branch expand |
root-tree-view.collapse |
{ value } |
Fired on branch collapse |
Extends
HTMLElement
- use with <section is="root-tree-view">. The keyboard model is the ARIA tree
pattern: arrows move and open, Home and End jump, Enter and Space toggle. A row whose content is a link
keeps that link's own tab stop, so a tree of links is still navigable one link at a time.