Trees
Pages under pages, a menu's items, folders: a list whose items open their own lists, each a link or a name, and moved by a handle.
Examples
tree on the list, tree-item on each item, its tree-row holding a tree-toggle - a --toggle button naming the item's tree-group in aria-controls, as on collapse - and the item's link. The groups close as hidden="until-found", so the browser's find in page still opens them. A button with command="--expand-all" or "--collapse-all" for the tree opens or closes every group that holds items.
<div class="hstack gap-8">
<button class="btn btn-sm" type="button" commandfor="tree-pages-list" command="--expand-all"><i class="bi bi-arrows-expand" aria-hidden="true"></i>Expand all</button>
<button class="btn btn-sm" type="button" commandfor="tree-pages-list" command="--collapse-all"><i class="bi bi-arrows-collapse" aria-hidden="true"></i>Collapse all</button>
</div>
<ul class="tree" id="tree-pages-list" aria-label="Pages">
<li class="tree-item">
<div class="tree-row">
<button class="btn btn-sm btn-icon btn-text tree-toggle" type="button" command="--toggle" aria-controls="tree-pages-excursions" aria-expanded="true" aria-label="Excursions"><i class="bi bi-chevron-right" aria-hidden="true"></i></button>
<a href="#tree-pages">Excursions</a>
</div>
<ul class="tree-group" id="tree-pages-excursions">
<li class="tree-item"><div class="tree-row"><a href="#tree-pages">North Bay by speedboat</a></div></li>
<li class="tree-item"><div class="tree-row"><a href="#tree-pages">Night market and street food</a></div></li>
</ul>
</li>
<li class="tree-item">
<div class="tree-row">
<button class="btn btn-sm btn-icon btn-text tree-toggle" type="button" command="--toggle" aria-controls="tree-pages-islands" aria-expanded="false" aria-label="Islands"><i class="bi bi-chevron-right" aria-hidden="true"></i></button>
<a href="#tree-pages">Islands</a>
</div>
<ul class="tree-group" id="tree-pages-islands" hidden="until-found">
<li class="tree-item"><div class="tree-row"><a href="#tree-pages">Turtle Cove</a></div></li>
<li class="tree-item"><div class="tree-row"><a href="#tree-pages">Moon Cove</a></div></li>
</ul>
</li>
<li class="tree-item">
<div class="tree-row">
<button class="btn btn-sm btn-icon btn-text tree-toggle" type="button" command="--toggle" aria-controls="tree-pages-archive" aria-expanded="false" aria-label="Archive"><i class="bi bi-chevron-right" aria-hidden="true"></i></button>
<a href="#tree-pages">Archive</a>
</div>
<ul class="tree-group" id="tree-pages-archive" hidden="until-found" data-lazy data-docs-lazy></ul>
</li>
<li class="tree-item"><div class="tree-row"><a href="#tree-pages">Contacts</a></div></li>
</ul>Children from the server
A group with data-lazy and no items fires redz:expand on it when it first opens. A page that loads the items cancels it and answers with a promise: the toggle is busy until it settles, and a refusal closes the group again for another try. The Archive above answers after 400 ms.
document.addEventListener('redz:expand', (event) => {
event.preventDefault();
event.detail.respondWith(fetch('/pages?parent=' + event.target.dataset.parent)
.then((answer) => answer.text())
.then((items) => event.target.insertAdjacentHTML('beforeend', items)));
});
Moved by a handle
A tree with data-reorder moves its items as a list does - see reordering - and its menu of moves adds data-move="in", to the end of the group of the item above, and "out", after its parent; an item can go in where the item above has a tree-group, empty or not. data-parent names the hidden field of each item's parent, its value the item's key.
<ul class="tree" id="tree-menu-list" aria-label="The site's menu" data-reorder="menu[]" data-parent="menu_parent">
<li class="tree-item" data-value="home">
<div class="tree-row">
<button class="btn btn-sm btn-icon btn-text drag-handle" type="button" commandfor="tree-menu-moves" command="toggle-popover" aria-haspopup="menu" aria-label="Move: Home"><i class="bi bi-grip-vertical" aria-hidden="true"></i></button>
<span>Home</span>
</div>
<ul class="tree-group" id="tree-menu-home"></ul>
</li>
<li class="tree-item" data-value="tours">
<div class="tree-row">
<button class="btn btn-sm btn-icon btn-text drag-handle" type="button" commandfor="tree-menu-moves" command="toggle-popover" aria-haspopup="menu" aria-label="Move: Tours"><i class="bi bi-grip-vertical" aria-hidden="true"></i></button>
<span>Tours</span>
</div>
<ul class="tree-group" id="tree-menu-tours">
<li class="tree-item" data-value="boats">
<div class="tree-row">
<button class="btn btn-sm btn-icon btn-text drag-handle" type="button" commandfor="tree-menu-moves" command="toggle-popover" aria-haspopup="menu" aria-label="Move: Boats"><i class="bi bi-grip-vertical" aria-hidden="true"></i></button>
<span>Boats</span>
</div>
<ul class="tree-group" id="tree-menu-boats"></ul>
</li>
</ul>
</li>
<li class="tree-item" data-value="contacts">
<div class="tree-row">
<button class="btn btn-sm btn-icon btn-text drag-handle" type="button" commandfor="tree-menu-moves" command="toggle-popover" aria-haspopup="menu" aria-label="Move: Contacts"><i class="bi bi-grip-vertical" aria-hidden="true"></i></button>
<span>Contacts</span>
</div>
<ul class="tree-group" id="tree-menu-contacts"></ul>
</li>
</ul>
<div class="menu" id="tree-menu-moves" popover role="menu" aria-label="Move">
<button class="menu-item" type="button" role="menuitem" data-move="up"><i class="bi bi-arrow-up" aria-hidden="true"></i>Move up</button>
<button class="menu-item" type="button" role="menuitem" data-move="down"><i class="bi bi-arrow-down" aria-hidden="true"></i>Move down</button>
<button class="menu-item" type="button" role="menuitem" data-move="in"><i class="bi bi-arrow-return-right flip-rtl" aria-hidden="true"></i>Move into the item above</button>
<button class="menu-item" type="button" role="menuitem" data-move="out"><i class="bi bi-arrow-bar-left flip-rtl" aria-hidden="true"></i>Move out</button>
</div>The current page, actions and checks
A row whose link has aria-current - the page open now - is tinted and its link bold. tree-actions at the end of a row holds its buttons: quiet until the row is hovered, holds the focus or has a menu open on a pointer screen, always shown on a touch screen. A check in a row posts by its own name, for a batch over the chosen pages. A search with data-filter keeps the parents of a match.
<input class="form-control" type="search" data-filter="tree-current-list" aria-label="Find a page" placeholder="Find a page" autocomplete="off">
<ul class="tree" id="tree-current-list" aria-label="Pages to publish">
<li class="tree-item">
<div class="tree-row">
<input class="check" type="checkbox" name="pages[]" value="excursions" aria-label="Choose: Excursions">
<a href="#tree-current">Excursions</a>
<span class="tree-actions"><button class="btn btn-xs btn-icon btn-text" type="button" aria-label="Add a page under Excursions"><i class="bi bi-plus-lg" aria-hidden="true"></i></button></span>
</div>
<ul class="tree-group">
<li class="tree-item">
<div class="tree-row">
<input class="check" type="checkbox" name="pages[]" value="north-bay" aria-label="Choose: North Bay by speedboat" checked>
<a href="#tree-current" aria-current="page">North Bay by speedboat</a>
<span class="tree-actions"><button class="btn btn-xs btn-icon btn-text" type="button" aria-label="Add a page under North Bay by speedboat"><i class="bi bi-plus-lg" aria-hidden="true"></i></button></span>
</div>
</li>
<li class="tree-item">
<div class="tree-row">
<input class="check" type="checkbox" name="pages[]" value="night-market" aria-label="Choose: Night market and street food">
<a href="#tree-current">Night market and street food</a>
<span class="tree-actions"><button class="btn btn-xs btn-icon btn-text" type="button" aria-label="Add a page under Night market and street food"><i class="bi bi-plus-lg" aria-hidden="true"></i></button></span>
</div>
</li>
</ul>
</li>
</ul>Keyboard and accessibility
- The tree is a list of lists with buttons and links - the disclosure pattern: Tab goes from one control to the next, Enter or Space opens or closes a group, and the toggle says it in
aria-expanded. It is not the ARIA tree widget, which the APG itself keeps for choosing, not for links. - A toggle is named after its item; its chevron points along the line of reading, mirrored right to left.
- The moves are a menu on the handle, so a keyboard, a screen reader and a single tap move an item as a drag does; after a move the focus stays on the moved item's handle and its new place is said politely.
Tokens
From css/components/tree.css, which declares no custom property of its own.
It reads the theme's --redz-space-12, --redz-space-8, --redz-border, --redz-space-4, --redz-control-height, --redz-control-height-sm, --redz-radius-6, --redz-primary-subtle, --redz-text, --redz-motion, --redz-ease.