Naar de inhoud

dvlopcore toolkit

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.

De toegankelijke naam van de boom, een tekst; of labelledby. Een van de twee is verplicht.

Het id van de geselecteerde knoop; zijn voorouders worden open getoond. In deze demo een van de knoop-id's van de demo.

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).

comfortable: rijen van 44 CSS-pixels; compact: lagere rijen en een kleinere inspringing.

rtl of ltr wanneer de richting van de boom van de pagina afwijkt; Rechts en Links wisselen in een boom van rechts naar links.

Herstellen

De demo toont de beginwaarden.

Eigenschappen

EigenschapTypeStandaardToegestane waardenOmschrijving
idstringverplichtHet id van de boom; de element-id's van de knopen zijn <id>-<n>, hun labels <id>-<n>-label.
nodesarrayverplichtDe 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.
labelstringgeenDe toegankelijke naam van de boom, een tekst; of labelledby. Een van de twee is verplicht.
labelledbystringgeenHet id van een kop die de boom benoemt, in plaats van label.
selectedstringgeenwindow, menubar, menu-file, menu-edit, menu-help, toolbar, new, save, search, content, form, field-name, field-amount, field-notes, actions, ok, cancel, statusbarHet id van de geselecteerde knoop; zijn voorouders worden open getoond. In deze demo een van de knoop-id's van de demo.
expandedarraygeenId's van knopen die open getoond worden naast de voorouders van de geselecteerde knoop.
selectionFollowsFocusbooltrueWaar: 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).
srcstringgeenEen URL-sjabloon met {id}; het script haalt de kinderen van een luie knoop daar op als JSON wanneer geen luisteraar dv-tree:load beantwoordt.
densitystringcomfortablecomfortable, compactcomfortable: rijen van 44 CSS-pixels; compact: lagere rijen en een kleinere inspringing.
langstringgeenDe taal van de tekst van alle knopen wanneer die van de pagina afwijkt.
dirstringgeenltr, rtlrtl of ltr wanneer de richting van de boom van de pagina afwijkt; Rechts en Links wisselen in een boom van rechts naar links.
stringsarrayverplichtDe interfaceteksten in de taal van de pagina: tree_expand, tree_collapse, tree_loading, tree_loaded, tree_failed.

Gebeurtenissen

GebeurtenisDetailOmschrijving
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

ToetsenDoet
Arrow Down, Arrow UpDe volgende, de vorige zichtbare knoop.
Arrow RightEen dichte tak: uitklappen; een open tak: zijn eerste kind; een blad: niets.
Arrow LeftEen 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.
EnterActiveert de knoop (dv-tree:activate) en volgt dan zijn link als hij er een heeft.
SpaceSelecteert de knoop met focus.
LettersVooruit 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.
TabKomt de boom één keer in en uit: één knoop heeft de tabindex, de geselecteerde, anders de eerste.

Toegankelijkheid

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

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:activate in 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:select with trigger: "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.