adobe / react-spectrum

A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
https://react-spectrum.adobe.com
Apache License 2.0
12.75k stars 1.09k forks source link

Implement TreeView component #2396

Open devongovett opened 3 years ago

devongovett commented 3 years ago

🙋 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.

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

GrantRussell commented 2 years ago

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.

curran commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 11 months ago

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

curran commented 11 months ago

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 commented 11 months ago

Is there any reason for not to use React Stately?

curran commented 11 months ago

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 commented 11 months ago

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 commented 11 months ago

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

GermanJablo commented 9 months ago

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 commented 8 months ago

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 commented 4 months ago

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 commented 4 months ago

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