# TreeView
A framework-agnostic, WCAG-conformant **tree view** in vanilla TypeScript. It is a
faithful, heavily-documented implementation of the [WAI-ARIA APG *Tree View*
pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/), written so it can
double as a reference you hand to an LLM (or a person) to reproduce in React,
Vue, Svelte, etc. without losing accessibility semantics.
The class **enhances** semantic nested-list markup rather than generating it, so
the tree degrades gracefully to a plain nested list without JavaScript.
## Markup contract
Provide nested `
`/`- ` lists. Each item is an `
- `
whose visible text lives in a `.treeview__label` child; a **parent** item also
contains a `
` of child items.
```html
```
- A node is a **parent** iff its `- ` has a direct `.treeview__group` child.
- Mark a parent open initially with `data-expanded` on its `
- `.
- Mark a node selected initially with `data-selected` on its `
- ` (only
meaningful when `selectionMode` is not `"none"`).
- You do **not** author any ARIA or ids — the class wires up `role`,
`aria-expanded`, `aria-selected`, `aria-level`, `aria-setsize`,
`aria-posinset`, `hidden`, roving `tabindex`, and generates missing ids.
## Usage
```ts
import { TreeView } from './TreeView';
import './treeview.css';
const root = document.querySelector('[data-treeview]')!;
const tree = new TreeView(root, { selectionMode: 'single', label: 'File system' });
root.addEventListener('treeview:activate', (e) => {
const { index, item } = (e as CustomEvent).detail;
// e.g. navigate to the resource represented by `item`
});
```
Passing a non-`HTMLElement` root throws a `TypeError`; a treeitem without a
`.treeview__label` child throws an `Error`.
## Options
All options are optional. Defaults match the APG's baseline example.
| Option | Type | Default | Description |
| --------------- | ------------------------------------- | -------------- | ----------- |
| `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'single'` | `none`: navigation only, no `aria-selected`. `single`: one node selected. `multiple`: any number selected; root gets `aria-multiselectable="true"`. |
| `wrapFocus` | `boolean` | `false` | Wrap Up/Down focus around the first/last visible node. The APG example does not wrap. |
| `typeAhead` | `boolean` | `true` | Focus the next visible node whose label starts with the typed characters. |
| `idPrefix` | `string` | `'treeview'` | Prefix for generated element ids. |
| `label` | `string` | `''` | Applied as `aria-label` on the root **only** if the markup has no `aria-label`/`aria-labelledby`. |
## Events
All events are `CustomEvent`s dispatched on the root element and **bubble**.
Events fired during initial setup are suppressed.
| Event | `detail` | Fired when |
| -------------------- | ---------------------------------------------------------- | ---------- |
| `treeview:toggle` | `{ index, expanded, item, group }` | A parent expands or collapses. |
| `treeview:select` | `{ index, selected, item }` | A node's selection state changes. |
| `treeview:activate` | `{ index, item }` | A node's primary action runs (click / Enter / Space). |
`index` is the zero-based document-order position of the node; `item` is its
`
- `; `group` is the child `
`.
## Public API
| Member | Description |
| ----------------------- | ----------- |
| `size` | Number of treeitems. |
| `isExpanded(index)` | Whether the node is an expanded parent. |
| `isSelected(index)` | Whether the node is selected. |
| `expand(index)` | Expand a parent (no-op for leaves / already open). |
| `collapse(index)` | Collapse a parent (no-op for leaves / already closed). |
| `toggle(index)` | Toggle a parent's expansion. |
| `select(index)` | Select a node (clears others in single mode). |
| `deselect(index)` | Deselect a node. |
| `focusItem(index)` | Move keyboard focus to a node, if currently visible. |
| `destroy()` | Remove all listeners and clear pending timers. |
## Accessibility contract
### Roles
| Element | Role |
| ------------------------------ | -------- |
| Root list (`[data-treeview]`) | `tree` |
| Each `.treeview__item` | `treeitem` |
| Each nested `.treeview__group` | `group` |
### States & properties (per treeitem)
| Attribute | Applied to | Meaning |
| -------------------------------- | ---------- | ------- |
| `aria-expanded` | parents only | `true`/`false` — leaf nodes never get it. |
| `aria-selected` | when selection enabled | `true`/`false`. |
| `aria-level` | all | 1-based depth. |
| `aria-setsize` / `aria-posinset` | all | Position within the group of siblings. |
| roving `tabindex` | all | Exactly one node is `0`; the rest are `-1`. Tab is a single stop into/out of the tree. |
Collapsed groups are hidden with the native `hidden` attribute, removing their
items from both the tab order and the accessibility tree.
### Keyboard interaction
All of the following are **required** by the APG for this pattern (tree
navigation is not optional the way the Accordion's arrow keys are).
| Key | Action |
| ------------------ | ------ |
| Down / Up Arrow | Move focus to the next / previous visible node. |
| Right Arrow | Collapsed parent → expand; expanded parent → focus first child; leaf → nothing. |
| Left Arrow | Expanded parent → collapse; otherwise → focus parent. |
| Home / End | Move focus to the first / last visible node. |
| Enter / Space | Primary action: toggle a parent, apply selection, emit `treeview:activate`. |
| Type a character | Focus the next visible node whose label starts with the typed string. |
| `*` (asterisk) | Expand every sibling parent at the current level. |
## Styling notes
The bundled `treeview.css` is deliberately minimal — indentation, a focus ring,
and ARIA-driven expand/collapse and selection affordances only. Override freely.
- **Never set `display` on `.treeview__group`.** Collapsed groups rely on the
`hidden` attribute (`[hidden] { display: none }` from the UA stylesheet); a
`display` rule would override it and break hiding.
- The **treeitem `- `**, not the label, is the focusable element, so the
focus indicator (`:focus-visible`) belongs on `.treeview__item`. Never remove it.
- Expand/collapse (`▸`/`▾`) and selection styling are driven purely by the ARIA
attributes, so the visuals always match what assistive tech announces.