Skip to content
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

Implement TreeView component #2396

Open
devongovett opened this issue Sep 29, 2021 · 15 comments
Open

Implement TreeView component #2396

devongovett opened this issue Sep 29, 2021 · 15 comments
Assignees
Labels
enhancement New feature or request rsp:TreeView

Comments

@devongovett
Copy link
Member

🙋 Feature Request

A TreeView allows users to navigate a hierarchical tree data structure, with expanding and collapsing items. It also allows multiple or single selection of items, focusable elements inside items, and other interactions.

ARIA pattern

Since TreeView allows focusable elements inside items, it will implement the treegrid ARIA pattern rather than the tree pattern. The TreeView Spectrum component supports only a single column, however, the React Aria hooks will not have this limitation.

Since grid and treegrid are so similar, the existing React Aria grid hooks should be extended with treegrid support. See the above pattern and this example for details.

  • Update useGridState to handle expanding and collapsing
  • Add a prop to useGrid that controls whether it is a treegrid. Basically changes the role. Cannot be changed after initial render (we should throw).
  • In useGridRow, we need to add aria-expanded for parent rows, aria-level, aria-setsize, and aria-posinset.
  • Add keyboard handling for left and right arrow keys to expand and collapse a row

Spectrum

React Spectrum’s TreeView component should follow the existing collections API, which allows nested items in the dynamic case. Static collections with nested children will not be supported for the initial version.

The Virtualizer should be used to implement TreeView as with other collection components, and a new TreeLayout will be needed to handle the layout. It should produce a flattened list of items with indentation since the treegrid pattern does not use DOM hierarchy.

TreeView should use the useGrid and associated hooks, and use the option that specifies that it is a treegrid. It should use useTreeState and TreeCollection to store the state, which should be converted within the component to use useGridState similar to ListView and CardView.

Example

import {TreeView, Item} from '@react-spectrum/tree';

<TreeView items={items}>
  {item => 
    <Item childItems={item.children}>
      <FolderIcon />
      <Text>{item.name}</Text>
    </Item>
  }
</TreeView>

API

interface TreeViewProps<T> extends CollectionBase<T>, MultipleSelection, Expandable {}
interface SpectrumTreeViewProps<T> extends TreeViewProps<T>, StyleProps, DOMProps {}

Not for initial implementation

  • No static API
  • No Sections
  • No drill-down (Spectrum design)
  • No thumbnails (Spectrum design)
  • No t-shirt sizes
  • No drag and drop
  • No tooltips or truncating (to come)
  • No infinite scroll (different ticket)
  • No async loading (different ticket)
  • No additional keyboard shortcuts (Spectrum design)
@devongovett devongovett added the enhancement New feature or request label Sep 29, 2021
@GrantRussell
Copy link

GrantRussell commented Jun 8, 2022

I'm not totally familiar with the proposed aria-pattern above, but a nice-to-have feature would be to rescue focus from a tree node that has document focus but is then removed from the DOM.

Currently in React Spectrum v2 TreeView, the focus is lost when the focused element is removed.

@matthewdeutsch matthewdeutsch changed the title TreeView component Implement TreeView component Sep 8, 2022
@curran
Copy link

curran commented Apr 25, 2023

I'm interested in potentially working on this.

Here is the Spectrum design doc for tree view https://spectrum.adobe.com/page/tree-view/

Where is the "React Spectrum v2 TreeView" defined?

@LFDanLu
Copy link
Member

LFDanLu commented Apr 28, 2023

@curran Thank you for your interest! The team is currently revisiting this to make sure all the information is still pertinent and up to date, we hope to get back to you in a week or so. If you'd like, you could explore implementing Tree using React Aria Components (RAC) and its collections pattern in the meantime. We plan on converting our React Spectrum components to use this new collection pattern so we figure it would be easier to create TreeView for RAC rather than implementing it in React Spectrum first and converting it later. Since it is a RAC implementation, you don't need to match the visuals of the Spectrum Design doc. Instead, the treegrid ARIA pattern should be your guide for the DOM structure and navigation patterns.

Keep in mind that due to ongoing team priorities we won't have much time to actively support a contribution of this scale, but we'd appreciate any findings and progress you make here!

@curran
Copy link

curran commented Apr 28, 2023

Awesome, thanks for the note! Indeed, an implementation in react-aria should come first, and honestly that's probably where my contribution would end, as I plan to develop custom UI components at that level and not adopt react-spectrum itself.

I'm was considering attempting a recursive form of the existing useListBox pattern. I see there are some 2-level examples with a Section concept, but no 3-level examples. Will post here if I have any progress to show.

@donaldpipowitch
Copy link

Was looking for a react-aria based treeview solution as well. Found this great very recent tutorial (not related to react-aria) in case someone needs a custom solution in the meantime.

@rostero1
Copy link

I assume this ticket applies to RAC as well, right?

@curran
Copy link

curran commented Nov 7, 2023

I've been working on a tree component for navigating files.

image

One thing I would like to do is make it more accessible using react-aria, or ideally use an off-the-shelf tree component from RAC (react-aria-components) if it exists. In the mean time, I've got some good direction here for making it more accessible: VZCode issue #176: Keyboard Navigation & Accessibility. Looking forward to making it work!

@edoelas
Copy link

edoelas commented Nov 7, 2023

Is there any reason for not to use React Stately?

@curran
Copy link

curran commented Nov 8, 2023

Is there any reason for not to use React Stately?

For what in particular? What would that look like? It may be a good fit, but I'm not very familiar with it. I'm open to using React Stately if the benefits are clear. What would be the benefits of using it?

@edoelas
Copy link

edoelas commented Nov 8, 2023

I discovered the react spectrum project a week ago, so I am not sure which is the best way to develop new components. Take what I say with a grain of salt.
React stately provides hooks that handle most of the logic for you, in particular it implements useTreeState and useTreeData. This added to the Tree View examples from spectrum CSS should make easier to develop a tree component that behaves and looks according to the adobe spectrum guidelines.
I am not sure if the React Spectrum components are developed in a similar way or they reimplement all the logic. Also, to me it seems that React Aria and React Stately overlap a bit. I have the same feeling with React Spectrum and React Aria components. I do not fully understand the organization and goals of each project.

EDIT:
I have checked the dependencies of a react spectrum component and it seems that the logic is handled by React Aria and React Stately: @react-spectrum/table. In fact, in the examples of React Aria they also import React Stately: useTable.

@reidbarber
Copy link
Member

@edoelas This page may be helpful: https://react-spectrum.adobe.com/architecture.html

@GermanJablo
Copy link

In case some research on treeView components helps, here's what I found:

image

The best options I found are react-arborist and react-complex-tree. As a third honorable mention, the latter's creator is working on his "spiritual successor", headless-tree (still in alpha, and the author doesn't seem to be able to spend much time on it). The website is not mentioned in the GitHub repository, so here it is.

The arborist API seems better to me at first glance, although react-component-tree and headless-tree have a much smaller bundle size.

The following libraries have open issues regarding the implementation of a TreeView component:

@GermanJablo
Copy link

GermanJablo commented Jan 8, 2024

To discuss / receive feedback

1. Nested or flat DOM?

It should produce a flattened list of items with indentation since the treegrid pattern does not use DOM hierarchy.

I have read Aria's documentation on the treegrid pattern, and I understand that it is possible to implement with a nested or flat DOM (they mention aria-owns), am I understanding correctly?

If so, I would like to receive feedback on some observations:

1.1 Drop-target.

While a gridlist can have a dropTarget only in "before" or "after" which is usually displayed with a thin line before or after, a treegrid can have a third state which is "inside". Although I have seen applications that display the "inside" state by only putting the drop target item in a different color, what offers a better UX is to color all the descendants of the item on which it is about to be dropped, just as programs like Notion or VS Code do.

treegrid

This is possible to do with both a flat and a nested DOM. However, nesting may require accessing all descendants and adding some indicator to them in the markup.

Another related thing to keep in mind is that it should not be possible to drag and drop an item on a descendant item to the one being dragged (for example in the gif above you should not be able to DnD 1 inside 1.1.

1.2 Virtualization

If I'm not mistaken, it should be possible to virtualize both models, although it would surely be much easier with a flat model.

The creator of react-complex-tree is making the new headless-tree library as I mentioned in my comment above, precisely because he found complications virtualizing a nested DOM.

Some virtualization libraries have not been built with a nested DOM in mind, although in others it appears to be doable.

2. Drop "inside"

The useDragAndDrop event exposes dropPosition which can be "before" or "after" as I mentioned above.
What would be better?

  1. Add the third state "inside" that differs from "before" and "after" according to the number of pixels located on the edge of the element. Users could not change that number of pixels.
  2. Do not expose "inside", "before" or "after", and let each user calculate it however they want.
    I tried doing something like this with useDrop and it wasn't very difficult
  const [dropPosition, setDropPosition] = useState<
    "before" | "after" | "inside" | null
  >(null);
  const { dropProps, isDropTarget } = useDrop({
    ref: dropRef,
    onDropMove(ev) {
      if (!dropRef.current) return;
      const itemHeight = dropRef.current.offsetHeight;
      if (ev.y < 7) setDropPosition("before");
      else if (ev.y > itemHeight - 7) setDropPosition("after");
      else setDropPosition("inside");
    },
  });
  1. Add the third state as in point 1 and also expose a property that allows defining the distance from which inside enters.

Edit: I have now discovered that the dragAndDrop hook used in gridlist has support for dropping inside the item as well as before and after. There is no option to set the distance in pixels, but it has been working well for me in my opinion.

3. Expanding and collapsing API

Using item selection as a reference, an analogous API could be:

type CollectionProps = {
   defaultCollapsedKeys: 'all' | Iterable<Key>, // initial state
   collapsedKeys: 'all' | Iterable<Key>, // current state
   onCollapsedChange: (keys: Collapsed) => any, // setter
}

However, for the same reasons I explained here I strongly suggest that the API be determined solely by a setter and a getter:

type CollectionProps = {
   getCollapsed: (item: object) => boolean, // getter
   setCollapsed: (item: object) => void, // setter
}

4. Name of the component

Since TreeView allows focusable elements inside items, it will implement the treegrid ARIA pattern rather than the tree pattern. The TreeView Spectrum component supports only a single column, however, the React Aria hooks will not have this limitation.

According to the Aria spec the patterns are called "Tree View" and "Treegrid". So wouldn't it be better to call the component TreeGrid/Treegrid to avoid confusion, since that is effectively the pattern being proposed here?

@6thpath
Copy link

6thpath commented May 12, 2024

Hi, I have a question on node selection for this component, as react-aria-components@1.2.0 released, i think when the parent node is selected then all children nodes should be selected too, is that right? or current behaviour is intended
Screenshot 2024-05-12 at 12 39 36

@LFDanLu
Copy link
Member

LFDanLu commented May 13, 2024

@6thpath At the moment that is intended behavior, similar to how macOS's file system allows you to select a parent folder individually from its children. I imagine we could consider adding support for customizing this behavior though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request rsp:TreeView
Projects
Status: No status
Development

No branches or pull requests