view_components icon indicating copy to clipboard operation
view_components copied to clipboard

Introduce the `TreeView` component

Open camertron opened this issue 7 months ago • 1 comments

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
  1. 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.
  2. 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
A screenshot of a fully expanded TreeView component showing a directory structure with 8 items A screenshot of a fully expanded TreeView component showing a directory structure with 8 items, all of which are checked A screenshot of a fully expanded TreeView component showing a directory structure with 8 items. The two outermost parents are in mixed state, while the grandchild and great-grandchild show checkmarks.

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

camertron avatar Jun 23 '25 04:06 camertron

🦋 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

changeset-bot[bot] avatar Jun 23 '25 04:06 changeset-bot[bot]

Hi people,

I just pushed some additional stuff that we already had implemented for this component:

  1. Disabled state for checkboxes. Disabled nodes cannot be activated, but are still focusable so as to be friendly to screen readers.
  2. Support for the TreeView as a form element. When a FormBuilder and a name is passed, a hidden input field is added which is updated when a selection is made.
  3. 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 Bildschirmfoto 2025-06-25 um 15 11 52

Mixed descendants Bildschirmfoto 2025-06-25 um 15 11 44

HDinger avatar Jun 25 '25 13:06 HDinger

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.

lesliecdubs avatar Jul 10 '25 03:07 lesliecdubs

Hi @lesliecdubs @jonrohan

do you have any updates on this? Is there anything we can support you with to make the review easier?

HDinger avatar Jul 23 '25 13:07 HDinger

@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 😄

camertron avatar Jul 30 '25 05:07 camertron