Skip to content

Introduce the TreeView component #3563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 30, 2025
Merged

Conversation

camertron
Copy link
Collaborator

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.

expand.mov

Loading spinner

Alt: a screen recording demonstrating the spinner that appears while sub-tree items are fetched from the server.

spinner.mov

Loading skeleton

Alt: a screen recording demonstrating the skeleton that appears while sub-tree items are fetched from the server.

skeleton.mov

Integration

This component is brand-new and thus should not affect any existing code in production.

Risk Assessment

  • 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

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Lookbook)
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

Co-authored-by: Cameron Dutro <[email protected]>
Copy link

changeset-bot bot commented Jun 23, 2025

🦋 Changeset detected

Latest commit: a9dc047

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

@camertron camertron force-pushed the feature/tree-view branch from 3adffd0 to 4e4c5ba Compare June 23, 2025 05:04
@camertron camertron marked this pull request as ready for review June 23, 2025 05:29
@camertron camertron requested review from a team as code owners June 23, 2025 05:29
@camertron camertron requested a review from langermank June 23, 2025 05:29
@HDinger
Copy link
Contributor

HDinger commented Jun 25, 2025

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

@lesliecdubs lesliecdubs requested a review from jonrohan July 10, 2025 03:39
@lesliecdubs
Copy link
Member

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.

@HDinger
Copy link
Contributor

HDinger commented Jul 23, 2025

Hi @lesliecdubs @jonrohan

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

Copy link
Member

@jonrohan jonrohan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution @camertron and sorry for the slow reply @HDinger. We were discussing internally on the choices of accepting such a large contribution and for a component TreeView which doesn't have an immediate product need for us. Balancing the risk of taking on the maintenance burden vs the reward of bringing the component library closer to the react counterpart.

If you could help making sure the CI is passing for lint errors I can merge this into the next release.

@camertron
Copy link
Collaborator Author

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

@jonrohan jonrohan added the skip changeset Pull requests that don't change the library output label Jul 30, 2025
@jonrohan jonrohan added this pull request to the merge queue Jul 30, 2025
Merged via the queue into primer:main with commit cf9ce00 Jul 30, 2025
25 of 29 checks passed
@primer primer bot mentioned this pull request Jul 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: TreeView skip changeset Pull requests that don't change the library output
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants