Introduce the `TreeView` component
What are you trying to accomplish?
This PR introduces TreeView, a new component modeled after the React component by the same name. TreeView is a hierarchical list of items that may have a parent-child relationship where children can be toggled into view by expanding or collapsing their parent item.
Terminology
Consider the following tree structure when reading through the list of terms:
src
├ button.rb
└ action_list
├ item.rb
└ header.rb
- Node. A node is an item in the tree. Nodes can either be "leaf" nodes (i.e. have no children), or "sub-tree" nodes, which do have children. In the example above, button.rb, item.rb, and header.rb are all leaf nodes, while action_list is a sub-tree node.
-
Path. A node's path is like its ID. It's an array of strings containing the current node's label and all the labels of its ancestors, in order. In the example above, header.rb's path is
["src", "action_list", "header.rb"].
Example
Here's how to render the example tree above using the TreeView component:
<%= render(Primer::OpenProject::TreeView.new) do |tree| %>
<% tree.with_leaf(label: "button.rb") %>
<% tree.with_sub_tree(label: "action_list") do |sub_tree| %>
<% sub_tree.with_leaf(label: "item.rb") %>
<% sub_tree.with_leaf(label: "header.rb") %>
<% end %>
<% end %>
Static vs dynamic nodes
TreeView sub-trees support fetching items from a remote server via one of two "loaders," either a spinner or a skeleton. Loaders accept a src: attribute with the URL to use to fetch items. The JSON-encoded path (i.e. array of strings) to the current node will be included as a GET parameter in the remote request.
<%= render(Primer::OpenProject::TreeView.new) do |tree| %>
<% tree.with_sub_tree(label: "action_list") do |sub_tree| %>
<%# choose one or the other: %>
<% sub_tree.with_loading_spinner(src: some_path_helper) %>
<% sub_tree.with_loading_skeleton(src: some_path_helper) %>
<% end %>
<% end %>
Visuals
Both leaf nodes and sub-tree nodes support leading and trailing visuals, leading actions, and checkboxes. Please see the documentation for all the relevant details. Here's an example of adding a trailing visual:
<%= render(Primer::OpenProject::TreeView.new) do |tree| %>
<% tree.with_leaf(label: "button.rb") do |leaf| %>
<% leaf.with_trailing_visual_icon(icon: :"diff-modified") %>
<% end %>
<% end %>
Checkbox support
TreeView supports checkboxes for leaf and sub-tree nodes. The component itself only handles the visual side of rendering checkboxes and their state, but does not track checked state, allow it to be submitted to the server, etc. Such behavior outside TreeView's scope of responsibility.
Checking a sub-tree node will check all of its children recursively, and unchecking it will uncheck all its children recursively. Sub-tree nodes with some checked and some unchecked children are said to be in a "mixed" state, represented by a horizontal line inside the checkbox UI element.
Screenshots
| Basic | Checked state | Mixed state |
|---|---|---|
Expanding/collapsing
Alt: a screen recording demonstrating how sub-tree nodes can be expanded and collapsed.
https://github.com/user-attachments/assets/2bf7e632-da62-4cfd-a1e3-0c2137d2e096
Loading spinner
Alt: a screen recording demonstrating the spinner that appears while sub-tree items are fetched from the server.
https://github.com/user-attachments/assets/67d71921-45ff-4bc2-b028-980f3c0ffcf8
Loading skeleton
Alt: a screen recording demonstrating the skeleton that appears while sub-tree items are fetched from the server.
https://github.com/user-attachments/assets/e03a1334-67f5-4da0-8a6d-b16bf3509dbb
Integration
This component is brand-new and thus should not affect any existing code in production.
Risk Assessment
- [x] Low risk the change is small, highly observable, and easily rolled back.
- [ ] Medium risk changes that are isolated, reduced in scope or could impact few users. The change will not impact library availability.
- [ ] High risk changes are those that could impact customers and SLOs, low or no test coverage, low observability, or slow to rollback.
Accessibility
- No new axe scan violation - This change does not introduce any new axe scan violations.
Merge checklist
- [x] Added/updated tests
- [x] Added/updated documentation
- [x] Added/updated previews (Lookbook)
- [x] Tested in Chrome
- [x] Tested in Firefox
- [x] Tested in Safari
- [x] Tested in Edge
🦋 Changeset detected
Latest commit: a9dc047da9f49539dd30a0375145ce2b7b57e8d6
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 1 package
| Name | Type |
|---|---|
| @primer/view-components | Minor |
Not sure what this means? Click here to learn what changesets are.
Click here if you're a maintainer who wants to add another changeset to this PR
Hi people,
I just pushed some additional stuff that we already had implemented for this component:
- Disabled state for checkboxes. Disabled nodes cannot be activated, but are still focusable so as to be friendly to screen readers.
- Support for the TreeView as a form element. When a
FormBuilderand anameis passed, a hidden input field is added which is updated when a selection is made. - The descendants select mode has been renamed mixed_descendants. The select mode now named descendants behaves slightly differently from its predecessor in that nodes are always either checked ("true") or unchecked ("false") and are never in an indeterminate/mixed state. The goal here was to allow treating sub-tree nodes as selectable entities in their own right rather than simply part of the hierarchy.
Descendants
Mixed descendants
Whoa, nice! Thanks @camertron and @HDinger for this big addition 🙇🏻♀️
I added another engineering reviewer to see about getting some more eyes on this soon.
Hi @lesliecdubs @jonrohan
do you have any updates on this? Is there anything we can support you with to make the review easier?
@jonrohan ah yeah, we thought that was probably the nature of the conversation on your side 😅
Thank you so much for reviewing and being willing to merge 💟
Also, I am willing to help with future maintenance of this component as time allows 😄