Liking cljdoc? Tell your friends :D

x-tree

A tree of hierarchical data. A person opens and closes branches and selects items, by pointer or by keyboard.


Tag name

<x-tree label="Files" selection="single" guides></x-tree>

<script type="module">
  const tree = document.querySelector('x-tree');
  tree.items = [
    { id: "src", label: "src", children: [
      { id: "main", label: "main.cljs", meta: "4 KB" } ] },
    { id: "readme", label: "README.md" } ];
  tree.expanded = ["src"];
</script>

Observed Attributes

AttributeTypeDefaultNotes
itemsJSON string(none)A JSON array of top-level items, see Items.
expandedJSON string(none)A JSON array of the ids of open branches.
selectedJSON string(none)A JSON array of the ids of selected items.
selectionenum"single"none, single, leaf or multiple. An unknown value gives the default.
labelstring(none)The accessible name of the tree.
loading-textstring"Loading…"The text of the loading row.
guidesbooleanfalseDraw indent guides.

Properties

Each attribute has a property that reflects it: items, expanded, selected, selection, label, loadingText and guides.

PropertyTypeNotes
items, expanded, selectedarrayWriting an array, or JSON text that holds one, writes its JSON. Anything else removes the attribute. Reading parses the attribute into a new value, or gives null.

Items

FieldTypeRequiredNotes
idstringyesA stable key. expanded, selected and events name items by it.
labelstringyesThe text, the accessible name and the typeahead text.
childrenarraynoNested items, in order.
disabledbooleannoThe item cannot be selected. It still takes focus, opens and closes.
lazybooleannoThe children are not loaded yet.
metastringnoShort text after the label: a count, a size, a date.
  • A branch has non-empty children, or has lazy. It has a chevron and can open.
  • A leaf is every other item.
  • An end item has no children: every leaf, and a lazy branch before its children load.
  • The tree drops an item, with its children, when it has no id or label, or when an earlier item in tree order has the same id. It writes a console warning for each.

Ids the tree counts

The tree ignores an id that names no item, and an id in expanded that names a leaf. In selected it counts:

selectionCounts
noneNo id.
singleThe first id in tree order.
leafThe first leaf id in tree order.
multipleEvery end item id.

A write by the tree keeps every id that names no item, after the ids it counts, in their order. Such an id can name a child of a lazy branch that has not loaded yet. In single and leaf, a select writes the selected id only.


Behaviour

Activating a row

A click on a row, Enter and Space all activate it:

selectionActivate a branchActivate a leaf
noneOpen or close.Nothing.
singleSelect.Select.
leafOpen or close.Select.
multipleCheck or uncheck.Check or uncheck.

A click on the chevron opens or closes the branch in every mode. On a disabled item, select and check do nothing. Open and close still work.

Cascading in multiple

A branch is checked when all its enabled end items are selected, mixed when some are, and unchecked when none are. A branch whose end items are all disabled counts all of them. Checking a branch adds all its enabled end items. Unchecking removes them. A mixed branch checks. Disabled end items keep their state.

Focus

  • The tree is one Tab stop. The focused item has tabindex="0". All others have -1.
  • Every gesture focuses the item it names. Canceling an event does not undo the focus.
  • When focus leaves the tree, the focused item stays. Tab returns to it.
  • When the focused item is hidden or removed, focus goes to its nearest visible ancestor, else the first selected item or its nearest visible ancestor, else the first visible item.
  • Moving focus selects nothing and sends no event.

Lazy branches

A lazy branch that is open and has no children shows the loading row, and the tree sends x-tree-lazy-load. Add children and remove lazy. When there are no children, remove lazy and the item becomes a leaf.


Keyboard

KeyAction
↓ / ↑Focus the next / previous visible item.
→Closed branch: open. Open branch: focus the first child. Leaf: nothing.
←Open branch: close. Otherwise: focus the parent.
Home / EndFocus the first / last visible item.
Enter / SpaceActivate.
*Open every branch that shares the parent of the focused item, its own included.
A characterFocus the next visible item whose label starts with that character. Wraps.

Events

EventDetailCancelableFires when
x-tree-toggle{ id, nextExpanded, source }yesBefore the tree writes expanded.
x-tree-select{ id, changed, nextSelected, source }yesBefore the tree writes selected.
x-tree-change{ attribute }noAfter the tree wrote an attribute.
x-tree-lazy-load{ id }noAfter a lazy branch with no children opened.

All events bubble and are composed. The tree sends events for its own gestures only, never for a write by the consumer. A canceled request writes nothing and sends no x-tree-change. One gesture sends its events in this order: the request, x-tree-change, then x-tree-lazy-load.

  • id is the item the gesture names. For * it is the focused item, and one x-tree-toggle opens every sibling branch.
  • nextExpanded and nextSelected are the JSON text the tree writes: the counted ids in tree order, then the kept ids.
  • changed is an array of every id that enters or leaves selected.
  • attribute is "expanded" or "selected". The property of that name holds the new value.
  • source is "pointer" or "keyboard".

The request of x-tree-toggle is expanded, and of x-tree-select is selected. To answer a request yourself, cancel it and write the attribute. listen from BareMirror cancels a request when the attribute does not show the exact text of nextExpanded or nextSelected after the dispatch. Write that text as it is, or write the property with the parsed array. The same ids in another order are other text.

tree.addEventListener('x-tree-select', e => {
  if (e.detail.changed.includes('locked')) e.preventDefault();
});

tree.addEventListener('x-tree-lazy-load', async e => {
  const children = await fetchChildren(e.detail.id);
  tree.items = withChildren(tree.items, e.detail.id, children);
});

CSS Custom Properties

PropertyDefault (light)Default (dark)Notes
--x-tree-font-familyvar(--x-font-family,inherit) Font of the rows
--x-tree-font-sizevar(--x-font-size-sm,0.875rem) Size of the rows
--x-tree-colorvar(--x-color-text,#1f2937)var(--x-color-text,#e5e7eb)Label text
--x-tree-meta-colorvar(--x-color-text-muted,#6b7280)var(--x-color-text-muted,#9ca3af)Meta text
--x-tree-row-height2rem Height of a row
--x-tree-row-padding0 var(--x-space-sm,0.5rem) Padding of a row
--x-tree-row-radiusvar(--x-radius-sm,4px) Corners of a row
--x-tree-indent1.25rem Indent per level
--x-tree-gap0.375rem Space between chevron, checkbox, label and meta
--x-tree-hover-bgcolor-mix(in srgb,var(--x-color-text,#000) 6%,transparent)color-mix(in srgb,var(--x-color-text,#fff) 6%,transparent)Row on hover
--x-tree-selected-bgcolor-mix(in srgb,var(--x-color-primary,#2563eb) 12%,transparent)color-mix(in srgb,var(--x-color-primary,#60a5fa) 20%,transparent)Selected row
--x-tree-selected-colorvar(--x-tree-color) Selected row text
--x-tree-focus-ringvar(--x-color-focus-ring,#2563eb)var(--x-color-focus-ring,#60a5fa)Focus outline
--x-tree-disabled-opacity0.5 Disabled row
--x-tree-chevron-colorvar(--x-color-text-muted,#6b7280)var(--x-color-text-muted,#9ca3af)Chevron
--x-tree-chevron-size1rem Chevron size
--x-tree-checkbox-colorvar(--x-color-primary,#2563eb)var(--x-color-primary,#60a5fa)Checkbox fill when checked or mixed
--x-tree-checkbox-size1rem Checkbox size
--x-tree-guide-widthvar(--x-border-width,1px) Guide width
--x-tree-guide-colorvar(--x-color-border,#e5e7eb)var(--x-color-border,#374151)Guide
--x-tree-guide-stylesolid Guide line style
--x-tree-guide-active-colorvar(--x-tree-guide-color) The guide of the group that holds the focused item
--x-tree-transition-durationvar(--x-transition-duration,150ms) Chevron rotation and colour change
x-tree {
  --x-tree-indent: 1rem;
  --x-tree-guide-active-color: var(--x-color-primary);
}

The host has data-empty while no item is valid:

x-tree[data-empty] { display: none; }

Shadow DOM Parts

PartElementNotes
tree<ul role="tree">The root list.
group<ul role="group">The children of an item. Draws the guide. data-active on the active guide. hidden while empty.
item<li role="treeitem">data-level, data-branch, data-expanded, data-selected, data-disabled. data-selected marks a selected or a checked item.
row<div>The line a person clicks.
chevron<span>Branches only. Rotates when open.
checkbox<span>multiple only. data-state is checked, mixed or unchecked.
label<span>The label text.
meta<span>Only with meta.
loading<li>The loading row.
x-tree::part(meta) { font-variant-numeric: tabular-nums; }

Writes on other nodes

None. The tree writes only its shadow root and its own expanded and selected.


Accessibility

  • The root list has role="tree" and aria-label from label. In multiple it has aria-multiselectable="true".
  • Each item has role="treeitem", aria-level, aria-setsize and aria-posinset.
  • A branch has aria-expanded. A leaf does not.
  • In single and leaf, each selectable item, a disabled one too, has aria-selected: true or false. In multiple, each item has aria-checked: true, false or mixed.
  • A disabled item has aria-disabled="true".
  • A child list has role="group".
  • A branch that shows the loading row has aria-busy="true". The loading row is not a treeitem.
  • Focus shows with :focus-visible. Selection shows with a fill and a checkbox, so colour is not the only cue in multiple.

Guides

With guides, each open group draws one line on its start edge, under the chevron of its branch. The line ends where the group ends. The guide of the group that holds the focused item draws in --x-tree-guide-active-color.

Motion

The chevron rotation and colour changes use --x-tree-transition-duration. Children appear and leave at once. Under prefers-reduced-motion: reduce the duration is 0ms.

Responsive

  • Rows are at least 44px tall under (pointer: coarse).
  • Long labels wrap. meta stays on the line end.

Size

The tree handles about 2000 items. The children of a closed branch are not in the DOM.


Usage Examples

Folders that open, files that select

<x-tree label="Files" selection="leaf" guides></x-tree>

Checkboxes

<x-tree label="Permissions" selection="multiple"></x-tree>
tree.addEventListener('x-tree-change', e => {
  if (e.detail.attribute === 'selected') save(tree.selected);
});

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close