Skip to content

Tree

<cae-tree> shows items in a tree, the children of each under it, folded and opened by the reader: files and directories, say. Register it with @contraagent/elements/tree/define; the types are in @contraagent/elements/tree.

docs/examples/tree.ts
// A tree of a CI pipeline's files, paired with a code element: selecting a file shows
// its text. The host holds the selection and answers `cae-select`.
import '@contraagent/elements/code/define';
import '@contraagent/elements/tree/define';
import type { TreeItem } from '@contraagent/elements/tree';
const files: Record<string, string> = {
'ci/build.py': 'def build(target: str) -> bool:\n return run(["make", target]).returncode == 0\n',
'ci/jobs.yaml': 'jobs:\n unit: { needs: [build] }\n lint: { needs: [] }\n',
'ci/tests/test_build.py': 'def test_build():\n assert build("all")\n',
'README.md': 'The CI pipeline of the project.\n',
};
const items: TreeItem[] = [
{
id: 'ci',
name: 'ci',
children: [
{ id: 'ci/build.py', name: 'build.py' },
{ id: 'ci/jobs.yaml', name: 'jobs.yaml', badge: 'changed', tone: 'attention' },
{ id: 'ci/tests', name: 'tests', open: false, children: [{ id: 'ci/tests/test_build.py', name: 'test_build.py', badge: 'failed', tone: 'failed' }] },
],
},
{ id: 'README.md', name: 'README.md' },
];
export function mount(host: HTMLElement): void {
const tree = document.createElement('cae-tree');
tree.items = items;
tree.label = 'Files';
const code = document.createElement('cae-code');
const show = (id: string) => {
tree.selected = id;
code.code = files[id] ?? '';
code.path = id;
code.language = id;
};
tree.addEventListener('cae-select', (e) => e.detail.id && show(e.detail.id));
show('ci/build.py');
const pair = document.createElement('div');
pair.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; align-items: start';
pair.append(tree, code);
host.replaceChildren(pair);
}

An item is a TreeItem: an id unique in the tree, a name and its children. Its icon is a directory for an item with children and a file for one without, unless icon names one. A badge is a word beside the name in the colour of its tone, with the tone’s mark (✓ done, ✕ failed, ◌ attention). open: false folds an item at first.

src/tree/tree.ts
/** One item of a tree: its name, the items under it, and how it looks. */
export interface TreeItem {
/** Its id, unique in the tree. */
id: string;
name: string;
/** Its icon: a directory for an item with children, a file for one without, unless given. */
icon?: 'file' | 'directory' | null;
/** A word beside the name, coloured by `tone` (with its mark: ✓ done, ✕ failed, ◌ attention). */
badge?: string | null;
tone?: Tone | null;
/** Its children are shown at first (default: true). */
open?: boolean | null;
children?: readonly TreeItem[] | null;
/** A click or Enter selects it (`cae-select`); default: an item with no children. An item with children that is not selectable opens and closes instead. */
selectable?: boolean | null;
}

selected is the selected item’s id; the element shows it and opens its ancestors, and the host sets it. A click, Enter or Space on a selectable item sends cae-select with its id: by default an item with no children is selectable, and an item with children opens and closes instead. The toggle before an item with children, Right and Left open and close it and send cae-toggle. A host pairs a tree with <cae-code> by showing the selected file’s text beside it (Code).

The tree is one tab stop, on the selected item or else the first. Up and Down move between the items shown, Right opens an item and then moves to its first child, Left closes it or moves to its parent, Home and End go to the first and the last item, and a letter moves to the next item whose name starts with it. A screen reader hears each item’s level, its place among its siblings, and whether it is open and selected.

The parts are tree, item (with data-id) and empty.