Boom
De boomweergave van een desktop-app op het web: knopen in niveaus, één geselecteerd, takken open of dicht, het toetsenbord van een desktop-boom. Een geneste lijst van <details> zonder JavaScript; het script maakt er een WAI-ARIA-boom van.
Demo: Van rechts naar links
חלון
סרגל תפריטים
- קובץ
- עריכה
- עזרה
סרגל כלים
- חדש
- שמירה
- חיפוש
חלונית תוכן
טופס
- שם
- כמות
- הערות
שורת כפתורים
- אישור
- ביטול
- שורת מצב
Eigenschappen bewerken
Wijzig een waarde en pas die toe; de demo wordt met de nieuwe waarden getoond. Een geweigerde waarde wordt bij het veld gemeld en de demo houdt de beginwaarde.
Eigenschappen
| Eigenschap | Type | Standaard | Toegestane waarden | Omschrijving |
|---|---|---|---|---|
id | string | verplicht | Het id van de boom; de element-id's van de knopen zijn <id>-<n>, hun labels <id>-<n>-label. | |
nodes | array | verplicht | De knopen: id, text, children per knoop; optioneel href (een link), lang, description (één korte zin), lazy (de kinderen komen later via dv-tree:load), expanded (wordt open getoond). Een leeg of dubbel id wordt geweigerd. | |
label | string | geen | De toegankelijke naam van de boom, een tekst; of labelledby. Een van de twee is verplicht. | |
labelledby | string | geen | Het id van een kop die de boom benoemt, in plaats van label. | |
selected | string | geen | window, menubar, menu-file, menu-edit, menu-help, toolbar, new, save, search, content, form, field-name, field-amount, field-notes, actions, ok, cancel, statusbar | Het id van de geselecteerde knoop; zijn voorouders worden open getoond. In deze demo een van de knoop-id's van de demo. |
expanded | array | geen | Id's van knopen die open getoond worden naast de voorouders van de geselecteerde knoop. | |
selectionFollowsFocus | bool | true | Waar: de pijltjes selecteren, zoals in een desktop-boom. Onwaar: de pijltjes verplaatsen alleen de focus; spatie, Enter of een klik selecteert (voor een inspector waarvan de selectie duur is). | |
src | string | geen | Een URL-sjabloon met {id}; het script haalt de kinderen van een luie knoop daar op als JSON wanneer geen luisteraar dv-tree:load beantwoordt. | |
density | string | comfortable | comfortable, compact | comfortable: rijen van 44 CSS-pixels; compact: lagere rijen en een kleinere inspringing. |
lang | string | geen | De taal van de tekst van alle knopen wanneer die van de pagina afwijkt. | |
dir | string | geen | ltr, rtl | rtl of ltr wanneer de richting van de boom van de pagina afwijkt; Rechts en Links wisselen in een boom van rechts naar links. |
strings | array | verplicht | De interfaceteksten in de taal van de pagina: tree_expand, tree_collapse, tree_loading, tree_loaded, tree_failed. |
Gebeurtenissen
| Gebeurtenis | Detail | Omschrijving |
|---|---|---|
dv-tree:select | { id, text, level, path, trigger } | De selectie ging naar deze knoop. trigger is keyboard, pointer of api. |
dv-tree:expand | { id, text, level, path, trigger } | Deze tak ging open, ook wanneer een voorouder openging voor een selectie. |
dv-tree:collapse | { id, text, level, path, trigger } | Deze tak ging dicht. |
dv-tree:activate | { id, text, level, path, trigger } | Enter, een dubbelklik of een klik op zijn link. Annuleerbaar: preventDefault() houdt de link tegen. |
dv-tree:load | { id, text, level, path, trigger, load(nodes) } | Een luie knoop gaat voor het eerst open; roep load() aan met een lijst knopen of een promise ervan. Zonder antwoord komen de kinderen van src, anders klapt de knoop weer in en zegt de statusregel dat. |
Toetsenbord
| Toetsen | Doet |
|---|---|
| Arrow Down, Arrow Up | De volgende, de vorige zichtbare knoop. |
| Arrow Right | Een dichte tak: uitklappen; een open tak: zijn eerste kind; een blad: niets. |
| Arrow Left | Een open tak: inklappen; anders de ouder, zonder die in te klappen. |
| Home, End (also with Ctrl) | De eerste, de laatste zichtbare knoop. |
| * | Klapt elke broer of zus van de knoop met focus uit. |
| Enter | Activeert de knoop (dv-tree:activate) en volgt dan zijn link als hij er een heeft. |
| Space | Selecteert de knoop met focus. |
| Letters | Vooruit typen: letters binnen een seconde vormen een voorvoegsel; één herhaalde letter gaat naar de volgende zichtbare knoop die ermee begint, rondlopend. Een tak gaat nooit open door een toetsaanslag. |
| Tab | Komt de boom één keer in en uit: één knoop heeft de tabindex, de geselecteerde, anders de eerste. |
Toegankelijkheid
- Zonder script een geneste lijst met een <details> per tak en het geselecteerde label aria-current; geen boomrol, toestand of tabindex wordt geclaimd.
- Met script: rollen tree, treeitem en group; aria-expanded op takken, aria-selected op de geselecteerde knoop, aria-level, aria-setsize en aria-posinset; een rondlopende tabindex.
- Een knoop heet naar zijn eigen label (aria-labelledby), nooit naar zijn hele tak; een omschrijving is een sr-only buur via aria-describedby.
- Bij een grove aanwijzer is het pijltje een benoemde knop (tree_expand, tree_collapse) voor schermlezers op aanraakschermen; elders is het voor hulptechnologie verborgen.
- Het enige live-gebied is de statusregel van het lui laden, door de server gezet wanneer de boom kan laden. Geforceerde kleuren gebruiken de systeemmarkering; beweging volgt --motion-duration; logische eigenschappen spiegelen een boom van rechts naar links.
Waarom zo
De webtegenhanger van MpTreeView van MiniProv, met dezelfde woorden (knoop, id, tekst, kinderen; selecteren, uitklappen, inklappen, focus), zodat de inspector van een desktopgereedschap op het web leest zoals op de desktop. De serveropmaak claimt geen boomrol en geen toestand die ze niet kan waarmaken: een dichte tak is een <details> die zonder script opengaat, een link blijft een link, het label van de geselecteerde knoop draagt aria-current. Het script verbetert tot het boompatroon met een rondlopende tabindex en de desktoptoetsen; de boomrol gaat voor aanraking nooit uit, want een apparaat met aanraking en toetsenbord houdt het toetsenbordmodel. Een knoop heet naar zijn eigen tekst, nooit naar zijn nakomelingen.
Verwijzingen
- MpTreeView
- UiTreeNode
- UiTreeNodes.Validate
- IUiLiveTreeWindow
- REG-P1-023
- WAI-ARIA APG tree view
Gebruikt
Dit component gebruikt geen ander component.
Gebruikt door
Nog geen gebruik vastgelegd.
README
tree
The kit's tree control, the web counterpart of MiniProv's MpTreeView (docs/architecture/concepts-and-terms.md):
nodes in levels, one selected, branches expanded or collapsed, the keyboard of a desktop tree. The HTML is a nested
list that works without JavaScript: a branch is a <details> whose <summary> is the node's row, so a collapsed
branch opens without the script; a leaf is a plain row; a node with an href is a link. tree.js turns the list
into the WAI-ARIA tree pattern (single select) and upgrades, never enables: the server markup claims no tree role,
no state and no tabindex. Made for the coming web inspector of a desktop app, whose navigation it will be.
Helper
dvlop_tree(array $props): string, pure, everything escaped, registers tree with core/assets.php.
| Prop | Meaning |
|---|---|
id |
the tree's id; the nodes' element ids are <id>-<n>, their labels <id>-<n>-label (required) |
nodes |
the nodes, see below |
label or labelledby |
the accessible name of the tree: a text, or the id of a heading (one is required) |
selected |
the id of the selected node; its ancestors render open |
expanded |
ids of nodes that render open besides those |
selectionFollowsFocus |
true (default): the arrows select, as a desktop tree; false: the arrows move focus only and Space, Enter or a click selects (for an inspector whose selection is costly) |
src |
a URL template with {id}; the script fetches the children of a lazy node there as JSON when no listener answers dv-tree:load |
density |
comfortable (default) or compact |
lang |
the language of all the nodes' text when it differs from the page's |
dir |
rtl or ltr when the tree's direction differs from the page's |
strings |
the interface strings of the page's language: tree_expand, tree_collapse, tree_loading, tree_loaded, tree_failed (required) |
The helper throws an InvalidArgumentException for a node without an id, an id two nodes share (as
UiTreeNodes.Validate does), a node without text, a selected id that is not in the tree, a tree without a name or
without the strings, and a density that is not one.
The node
['id' => ..., 'text' => ..., 'children' => [...]], MiniProv's UiTreeNode(Id, Text, Children). id is the
node's identity: unique in the whole tree and stable across refreshes (a later live tree reconciles by it); text is
what the node says and is its accessible name, never its descendants'. Optional: href (the node is a link; Enter and
a click follow it), lang (the text is in another language than the page; written on the label), description (one
short sentence, aria-describedby), lazy (the children come later through dv-tree:load; a node with children
given is never lazy), expanded (renders open).
Initial expansion, as MpTreeView: everything collapsed except the ancestors of the selected node and the nodes
given expanded. Selecting a node in code expands its ancestors.
Markup
div.dv-tree[#id][lang][dir][data-selection=manual][data-src][data-expand ...]
ul.dv-tree-group.dv-tree-root[aria-label | aria-labelledby] (role=tree with the script)
li.dv-tree-item[#id-n][data-id][data-lazy] (role=treeitem, aria-expanded/selected/level/setsize/posinset, tabindex)
details.dv-tree-branch[open] > summary.dv-tree-row (a branch; the script makes it div.dv-tree-row + ul.dv-tree-group[role=group])
div.dv-tree-row (a leaf or a lazy node)
span.dv-tree-toggle (the chevron's column; a button on a branch with the script)
span|a.dv-tree-label[#id-n-label][href][lang] (the text; aria-labelledby of the item points here)
span.sr-only[#id-n-description] (the description, a sibling)
div.dv-tree-status[role=status][aria-live=polite] (only with a lazy node or a src)
The chevrons and the guide lines are drawn by the stylesheet and never announced. The server marks the selected
node's label with aria-current="true" (a meaning the list has without the script); the script replaces it by
aria-selected on the item. The name of a treeitem is its own label through aria-labelledby, which holds in
Chrome, Firefox and Safari; a plain name from content would include the whole branch.
Keys (tree.js, one home for every tree)
| Key | Does |
|---|---|
| Down, Up | the next, the previous visible node |
| Right | a collapsed branch: expand; an expanded branch: its first child; a leaf: nothing |
| Left | an expanded branch: collapse; else: the parent, without collapsing it (the inspiration page collapses it, a fault to avoid) |
| Home, End, also with Ctrl | the first, the last visible node |
* |
expands every sibling of the focused node |
| Enter | activates the node (dv-tree:activate, then its link when it has one) |
| Space | selects the focused node |
| letters | type-ahead: characters typed within a second of each other are a prefix; one letter, or the same letter repeated, moves to the next visible node whose text starts with it (wrapping round); a longer prefix keeps the node when it still matches, else moves to the next match; case-insensitive on text; only visible nodes, a branch never opens on a keystroke; no match: stay, say nothing |
Right and Left swap in a right-to-left tree (dir="rtl" on the tree or an ancestor). Selection follows focus by
default; with selectionFollowsFocus => false the arrows, Home, End and type-ahead move focus only. Exactly one node
has tabindex="0": the selected one, else the first, else the one the keyboard was last on; collapsing a branch that
holds it moves it to the branch (and the selection too when it follows focus, as a desktop tree does).
Pointer: the chevron toggles (its column is one indent wide and a row tall, at least 24 by 24 CSS pixels; comfortable
rows are 44 pixels); a click on the row selects and focuses; a double click activates; a click on a link follows it
(after dv-tree:activate, which can cancel it).
Touch screen readers (VoiceOver on iOS, TalkBack) cannot send arrow keys. On a coarse pointer ((pointer: coarse),
re-evaluated when it changes) the chevron is a named button (tree_expand, tree_collapse) they can double-tap, and
a double tap on the row selects. Elsewhere the chevron is hidden from assistive technology and the treeitem's own
state says expanded or collapsed. The tree role never switches off, unlike the inspiration page: a device with touch
and a keyboard keeps the keyboard model, and a mode guessed from ontouchstart is wrong on such a device.
Announcements come from the native tree semantics (name, level, position, expanded, selected). The only live region
is the status line of lazy loading: tree_loading, tree_loaded with the count, tree_failed.
Events
CustomEvents dispatched on the .dv-tree element, bubbling. Detail, the same shape for every event:
{ id, text, level, path, trigger }
id the node's id
text its text
level 1 for a top node
path the ids from the top down to this node, the node's own last
trigger 'keyboard' | 'pointer' | 'api'
| Event | When |
|---|---|
dv-tree:select |
the selection moved to this node |
dv-tree:expand |
this branch opened (also when an ancestor opened for a selection) |
dv-tree:collapse |
this branch closed |
dv-tree:activate |
Enter, a double click, or a click on its link; cancelable: preventDefault() keeps the link from being followed |
dv-tree:load |
a lazy node opens for the first time; the detail also holds load(nodes): call it during the event with an array of nodes or a promise of one. When nobody calls it and the tree has src, the children are fetched from src with {id} replaced (JSON: an array of nodes). The item is aria-busy meanwhile; a failure collapses it again and the status line says so |
Instance API
element.dvTree on the .dv-tree element, the words of IUiLiveTreeWindow:
| Member | Does |
|---|---|
selectedId |
the id of the selected node, or null |
expandedIds |
the ids of the expanded nodes |
select(id) |
selects the node, expands its ancestors, moves the tabindex to it (and the focus, when the keyboard is on the tree); true when the id exists |
expand(id), collapse(id) |
opens or closes a branch (a lazy one loads); true when the id exists |
focus() |
puts the keyboard on the tree, on the node that holds the tabindex |
Events from the API carry trigger: 'api'. A later setNodes(nodes) (the live tree of UiLiveTreeWindow) fits
beside these; docs/architecture/STATUS-treeview.md describes it.
Style
tree.css, tokens only: --tree-row-height, --tree-indent, --tree-gap (each with a -compact value, read by
the compact variant), --tree-guide, --tree-hover-background and --tree-selected-background, which derive from
the page's colour tokens, so a site colours the tree through its own token overrides and dark token values give a
dark tree. A derived token is computed where it is declared (:root): a site that sets its colours in :root needs
nothing more; a wrapper that changes the colours for one part of a page states the three derived tree tokens again
(the demo's dark wrapper does). The focused row uses the focus tokens; selected and focused is distinct from selected without focus (the
tint and the accent bar). forced-colors: active uses system colours (the selected row Highlight on
HighlightText, the guides GrayText), --motion-duration is 0s under prefers-reduced-motion, and every
property is logical so dir="rtl" mirrors the tree.
Manual check list
The repository has no JavaScript test road, so the script's behaviour is checked by hand on the catalogue page
(examples/site, "Componenten" / "Components"), with the keyboard and a screen reader. Each line is one check.
Keyboard:
- [ ] Tab reaches the tree once, on the selected node ("Opslaan"); Shift+Tab leaves it; Tab again continues after it
- [ ] Down and Up walk the visible nodes only, stopping at the ends; the selection follows
- [ ] Right on a collapsed branch opens it; Right again goes to its first child; Right on a leaf does nothing
- [ ] Left on an expanded branch closes it; Left on a child goes to its parent and leaves the parent open
- [ ] Home and End, also with Ctrl, go to the first and the last visible node
- [ ]
*on "Menubalk" opens every sibling branch at that level - [ ] Enter on "Help" follows the link; Enter on "Opslaan" shows
dv-tree:activatein the panel - [ ] Space selects (in the dark tree, where the arrows only move focus)
- [ ] type "n": the next node starting with n; "n" again: the one after it (wrapping); "na" quickly: the node starting with "na"; a second later, "o": the next node starting with o
- [ ] type-ahead never opens a collapsed branch; a letter with no match leaves the focus where it is
- [ ] in the Hebrew tree Left opens and Right closes
- [ ] collapsing the branch that holds the selected node (Left on "Werkbalk") moves selection and focus to the branch
Screen reader (NVDA with Firefox and Chrome, VoiceOver on macOS):
- [ ] on focus a node says its own text, level, position in its set, expanded or collapsed, selected; never the texts of its children, never "button" for the chevron
- [ ] "Opslaan" also reads its description, once
- [ ] "Help" is a link inside the item: Enter follows it
- [ ] opening "Artikelen" says "Bezig met laden" then "3 onderdelen geladen" / the English equivalents; "Leveranciers" says "Laden is mislukt" and stays collapsed
- [ ] the tree's name ("Opbouw van het venster") is announced on entering; the Hebrew tree reads with a Hebrew voice and is named by its heading in the page's language
Touch screen reader (VoiceOver on iOS, TalkBack):
- [ ] swiping meets, per branch, a button "Uitklappen" / "Inklappen" and then the node; double-tapping the button toggles the branch
- [ ] double-tapping a node selects it; the panel shows
dv-tree:selectwithtrigger: "pointer"
Look:
- [ ] hover tints a row; the selected row is tinted with the accent bar at its start; the focused row shows the focus tokens; both at once are visibly different from selected alone
- [ ] forced colours (Windows high contrast): the selected row in the system highlight colours, the guides and the focus ring visible, the chevrons drawn
- [ ]
prefers-reduced-motion: the chevrons turn without a transition - [ ] 200% zoom and a phone width: rows wrap, nothing overflows, the panel moves under the tree
- [ ] the dark wrapper shows a dark tree with readable text, selection and focus
- [ ] the Hebrew tree is mirrored: chevrons at the right, indent to the left
- [ ] without JavaScript: the branches open and close as
<details>, the links work, the selected node is marked
Change log
- 2026-10-14: new, for the web inspector of a desktop app (order dvlopcore-treeview). Behaviour measured against Tim's ultimatetree page (keyboard versus touch, a separate expand button, activate versus expand, a status region) and improved: the tree role never switches off, Left never collapses the parent, children carry their position, type-ahead holds a prefix, the name of a node is its own text.