Skip to content

Latest commit

 

History

History
517 lines (448 loc) · 16.8 KB

File metadata and controls

517 lines (448 loc) · 16.8 KB

import {Layout} from '../../src/Layout'; export default Layout;

import docs from 'docs:react-aria-components'; import {Tree as VanillaTree, TreeItem} from 'vanilla-starter/Tree'; import vanillaDocs from 'docs:vanilla-starter/Tree'; import '../../tailwind/tailwind.css'; import Anatomy from 'react-aria-components/docs/TreeAnatomy.svg'; import {InlineAlert, Heading, Content} from '@react-spectrum/s2' import {VersionBadge} from '../../src/VersionBadge';

export const tags = ['data', 'tree', 'nested', 'hierarchy']; export const relatedPages = [ {title: 'Testing Tree', url: './Tree/testing'} ]; export const description = 'Provides users with a way to navigate nested hierarchical information.';

Tree

{docs.exports.Tree.description}

```tsx render docs={docs.exports.Tree} links={docs.links} props={['selectionMode']} initialProps={{selectionMode: 'multiple'}} type="vanilla" files={["starters/docs/src/Tree.tsx", "starters/docs/src/Tree.css"]} "use client"; import {Tree, TreeItem} from 'vanilla-starter/Tree';

<Tree /* PROPS */ aria-label="Files">


```tsx render docs={docs.exports.Tree} links={docs.links} props={['selectionMode']} initialProps={{selectionMode: 'multiple'}} type="tailwind" files={["starters/tailwind/src/Tree.tsx"]}
"use client";
import {Tree, TreeItem} from 'tailwind-starter/Tree';

<Tree
  /* PROPS */
  aria-label="Files">
  <TreeItem title="Documents">
    <TreeItem title="Project">
      <TreeItem title="Weekly Report" />
    </TreeItem>
  </TreeItem>
  <TreeItem title="Photos">
    <TreeItem title="Image 1" />
    <TreeItem title="Image 2" />
  </TreeItem>
</Tree>

Content

Tree follows the Collection Components API, accepting both static and dynamic collections. This example shows a dynamic collection, passing a list of objects to the items prop, and a recursive function to render the children.

"use client";
import {Tree, TreeItem} from 'vanilla-starter/Tree';
import {Collection} from 'react-aria-components/Collection';

///- begin collapse -///
let items = [
  {id: 1, title: 'Documents', type: 'directory', children: [
    {id: 2, title: 'Project', type: 'directory', children: [
      {id: 3, title: 'Weekly Report', type: 'file', children: []},
      {id: 4, title: 'Budget', type: 'file', children: []}
    ]}
  ]},
  {id: 5, title: 'Photos', type: 'directory', children: [
    {id: 6, title: 'Image 1', type: 'file', children: []},
    {id: 7, title: 'Image 2', type: 'file', children: []}
  ]}
];
///- end collapse -///

<Tree
  aria-label="Files"
  defaultExpandedKeys={[1, 4]}
  items={items}
  selectionMode="multiple">
  {function renderItem(item) {
    return (
      <TreeItem title={item.title}>
        {/*- begin highlight -*/}
        {/* recursively render children */}
        <Collection items={item.children}>
          {renderItem}
        </Collection>
        {/*- end highlight -*/}
      </TreeItem>
    );
  }}
</Tree>

Asynchronous loading

Use renderEmptyState to display a spinner during initial load. To enable infinite scrolling, render a <TreeLoadMoreItem> at the end of each <TreeItem>. Use whatever data fetching library you prefer – this example uses useAsyncList from react-stately.

"use client";
import {Tree, TreeItem, TreeLoadMoreItem} from 'vanilla-starter/Tree';
import {ProgressCircle} from 'vanilla-starter/ProgressCircle';
import {Collection} from 'react-aria-components/Collection';
import {useAsyncList} from 'react-aria-components/useAsyncList';

interface Character {
  name: string
}

function AsyncLoadingExample() {
  ///- begin collapse -///
  let starWarsList = useAsyncList<Character>({
    async load({signal, cursor}) {
      if (cursor) {
        cursor = cursor.replace(/^http:\/\//i, 'https://');
      }

      let res = await fetch(cursor || 'https://swapi.py4e.com/api/people/?search=', {signal});
      let json = await res.json();

      return {
        items: json.results,
        cursor: json.next
      };
    }
  });
  ///- end collapse -///

  ///- begin collapse -///
  let pokemonList = useAsyncList<Character>({
    async load({signal, cursor, filterText}) {
      let res = await fetch(
        cursor || `https://pokeapi.co/api/v2/pokemon`,
        {signal}
      );
      let json = await res.json();

      return {
        items: json.results,
        cursor: json.next
      };
    }
  });
  ///- end collapse -///

  return (
    <Tree
      aria-label="Async loading tree"
      style={{height: 300}}
      renderEmptyState={() => (
        <ProgressCircle isIndeterminate aria-label="Loading..." />
      )}>
      <TreeItem title="Pokemon">
        <Collection items={pokemonList.items}>
          {(item) => <TreeItem id={item.name} title={item.name} />}
        </Collection>
        {/*- begin highlight -*/}
        <TreeLoadMoreItem
          onLoadMore={pokemonList.loadMore}
          isLoading={pokemonList.loadingState === 'loadingMore'} />
        {/*- end highlight -*/}
      </TreeItem>
      <TreeItem title="Star Wars">
        <Collection items={starWarsList.items}>
          {(item) => <TreeItem id={item.name} title={item.name} />}
        </Collection>
        {/*- begin highlight -*/}
        <TreeLoadMoreItem
          onLoadMore={starWarsList.loadMore}
          isLoading={starWarsList.loadingState === 'loadingMore'} />
        {/*- end highlight -*/}
      </TreeItem>
    </Tree>
  );
}

Links

Use the href prop on a <TreeItem> to create a link. Link interactions vary depending on the selection behavior. See the selection guide for more details.

"use client";
import {Tree, TreeItem} from 'vanilla-starter/Tree';

<Tree
  /* PROPS */
  aria-label="Tree with links"
  defaultExpandedKeys={['bulbasaur', 'ivysaur']}>
  <TreeItem
    /*- begin highlight -*/
    href="https://pokemondb.net/pokedex/bulbasaur"
    target="_blank"
    /*- end highlight -*/
    id="bulbasaur"
    title="Bulbasaur">
    <TreeItem
      id="ivysaur"
      title="Ivysaur"
      href="https://pokemondb.net/pokedex/ivysaur"
      target="_blank">
      <TreeItem
        id="venusaur"
        title="Venusaur"
        href="https://pokemondb.net/pokedex/venusaur"
        target="_blank" />
    </TreeItem>
  </TreeItem>
</Tree>

<InlineAlert variant="notice" UNSAFE_style={{marginTop: '2rem'}}> Client-side routing Due to HTML spec limitations, TreeItems cannot be rendered as <a> elements. React Aria handles link clicks with JavaScript and triggers native navigation. When using a client-side router, use the onAction event to programmatically trigger navigation instead of the href prop.

Empty state

"use client";
import {Tree} from 'vanilla-starter/Tree';

<Tree
  aria-label="Search results"
  renderEmptyState={() => 'No results found.'}>
  {[]}
</Tree>

Sections

Use the <TreeSection> component to group options. A <TreeHeader> element may also be included to label the section. Sections without a header must have an aria-label.

"use client";
import {Tree, TreeHeader, TreeItem, TreeSection} from 'vanilla-starter/Tree';

<Tree aria-label="Files">
  <TreeSection>
    <TreeHeader>Photos</TreeHeader>
    <TreeItem id="my-photos" title="My Photos">
      <TreeItem id="photo-1" title="Photo 1" />
      <TreeItem id="photo-2" title="Photo 2" />
    </TreeItem>
    <TreeItem id="shared-photos" title="Shared Photos">
      <TreeItem id="shared-photo-1" title="Shared Photo 1" />
      <TreeItem id="shared-photo-2" title="Shared Photo 2" />
    </TreeItem>
  </TreeSection>
  <TreeSection>
    <TreeHeader>Documents</TreeHeader>
    <TreeItem id="my-documents" title="My Documents">
      <TreeItem id="document-1" title="Document 1" />
      <TreeItem id="document-2" title="Document 2" />
    </TreeItem>
    <TreeItem id="shared-documents" title="Shared Documents">
      <TreeItem id="shared-document-1" title="Shared Document 1" />
      <TreeItem id="shared-document-2" title="Shared Document 2" />
    </TreeItem>
  </TreeSection>
</Tree>

Selection and actions

Use the selectionMode prop to enable single or multiple selection. The selected items can be controlled via the selectedKeys prop, matching the id prop of the items. The onAction event handles item actions. Items can be disabled with the isDisabled prop. See the selection guide for more details.

"use client";
import type {Selection} from 'react-aria-components/Tree';
import {Tree, TreeItem} from 'vanilla-starter/Tree';
import {useState} from 'react';

function Example(props) {
  let [selected, setSelected] = useState<Selection>(new Set());

  return (
    <div>
      <Tree
        {...props}
        aria-label="Pokemon evolution"
        style={{height: 250}}
        defaultExpandedKeys={['bulbasaur', 'ivysaur']}
        ///- begin highlight -///
        /* PROPS */
        selectedKeys={selected}
        onSelectionChange={setSelected}
        onAction={key => alert(`Clicked ${key}`)}
        ///- end highlight -///
      >
        <TreeItem id="bulbasaur" title="Bulbasaur">
          <TreeItem id="ivysaur" title="Ivysaur">
            <TreeItem id="venusaur" title="Venusaur" isDisabled />
          </TreeItem>
        </TreeItem>
        <TreeItem id="charmander" title="Charmander">
          <TreeItem id="charmeleon" title="Charmeleon">
            <TreeItem id="charizard" title="Charizard" />
          </TreeItem>
        </TreeItem>
        <TreeItem id="squirtle" title="Squirtle">
          <TreeItem id="wartortle" title="Wartortle">
            <TreeItem id="blastoise" title="Blastoise" />
          </TreeItem>
        </TreeItem>
      </Tree>
      <p>Current selection: {selected === 'all' ? 'all' : [...selected].join(', ')}</p>
    </div>
  );
}

Keyboard navigation

By default, Tree uses arrow key navigation to move focus into rows. Set keyboardNavigationBehavior="tab" to have Option move focus in and out of a row. Use this when rows contain interactive elements such as text fields, where arrow keys and typing in the field should not trigger grid navigation or selection.

"use client";
import {Tree, TreeItem, TreeItemContent} from 'vanilla-starter/Tree';
import {ComboBox, ComboBoxItem} from 'vanilla-starter/ComboBox';

///- begin collapse -///
function PermissionPicker({label}) {
  return (
    <ComboBox style={{marginInlineStart: 'auto', flexShrink: 0}} aria-label={label} defaultSelectedKey="view" placeholder="Permission">
      <ComboBoxItem id="view">Can view</ComboBoxItem>
      <ComboBoxItem id="comment">Can comment</ComboBoxItem>
      <ComboBoxItem id="edit">Can edit</ComboBoxItem>
    </ComboBox>
  );
}
///- end collapse -///

<Tree
  /*- begin highlight -*/
  keyboardNavigationBehavior="tab"
  /*- end highlight -*/
  selectionMode="multiple"
  defaultExpandedKeys={['documents', 'photos']}
  aria-label="Shared files"
  style={{width: 420}}>
  <TreeItem id="documents" title="Documents">
    <TreeItem id="weekly" textValue="Weekly Report.pdf">
      <TreeItemContent>
        Weekly Report.pdf
        <PermissionPicker label="Weekly Report.pdf permission" />
      </TreeItemContent>
    </TreeItem>
    <TreeItem id="budget" textValue="Budget.xlsx">
      <TreeItemContent>
        Budget.xlsx
        <PermissionPicker label="Budget.xlsx permission" />
      </TreeItemContent>
    </TreeItem>
  </TreeItem>
  <TreeItem id="photos" title="Photos">
    <TreeItem id="sunset" textValue="Sunset.jpg">
      <TreeItemContent>
        Sunset.jpg
        <PermissionPicker label="Sunset.jpg permission" />
      </TreeItemContent>
    </TreeItem>
  </TreeItem>
</Tree>

Drag and drop

Tree supports drag and drop interactions when the dragAndDropHooks prop is provided using the hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the drag and drop guide to learn more.

"use client";
import {Tree, TreeItem} from 'vanilla-starter/Tree';
import {useDragAndDrop} from 'react-aria-components/useDragAndDrop';
import {Collection} from 'react-aria-components/Collection';
import {useTreeData} from 'react-aria-components/useTreeData';

function Example() {
  ///- begin collapse -///
  let tree = useTreeData({
    initialItems: [
      {id: '1', title: 'Documents', type: 'directory', children: [
        {id: '2', title: 'Project', type: 'directory', children: [
          {id: '3', title: 'Weekly Report', type: 'file', children: []},
          {id: '4', title: 'Budget', type: 'file', children: []}
        ]}
      ]},
      {id: '5', title: 'Photos', type: 'directory', children: [
        {id: '6', title: 'Image 1', type: 'file', children: []},
        {id: '7', title: 'Image 2', type: 'file', children: []}
      ]}
    ]
  });
  ///- end collapse -///

  ///- begin highlight -///
  let {dragAndDropHooks} = useDragAndDrop({
    getItems: (keys, items: typeof tree.items) => items.map(item => ({'text/plain': item.value.title})),
    onMove(e) {
      if (e.target.dropPosition === 'before') {
        tree.moveBefore(e.target.key, e.keys);
      } else if (e.target.dropPosition === 'after') {
        tree.moveAfter(e.target.key, e.keys);
      } else if (e.target.dropPosition === 'on') {
        // Move items to become children of the target
        let targetNode = tree.getItem(e.target.key);
        if (targetNode) {
          let targetIndex = targetNode.children ? targetNode.children.length : 0;
          let keyArray = Array.from(e.keys);
          for (let i = 0; i < keyArray.length; i++) {
            tree.move(keyArray[i], e.target.key, targetIndex + i);
          }
        }
      }
    }
  });
  ///- end highlight -///

  return (
    <Tree
      aria-label="Tree with hierarchical drag and drop"
      selectionMode="multiple"
      items={tree.items}
      ///- begin highlight -///
      dragAndDropHooks={dragAndDropHooks}
      ///- end highlight -///
    >
      {function renderItem(item) {
        return (
          <TreeItem title={item.value.title}>
            {item.children && <Collection items={item.children}>
              {renderItem}
            </Collection>}
          </TreeItem>
        )
      }}
    </Tree>
  );
}

Examples

API

<Tree>
  <TreeItem>
    <TreeItemContent>
      <Button slot="chevron" />
      <Checkbox slot="selection" /> or <SelectionIndicator />
      <Button slot="drag" />
    </TreeItemContent>
    <TreeItem>
      {/* ... */}
    </TreeItem>
  </TreeItem>
  <TreeSection>
    <TreeHeader />
    <TreeItem>{/* ... */}</TreeItem>
  </TreeSection>
  <TreeLoadMoreItem />
</Tree>

Tree

TreeItem

<PropTable component={docs.exports.TreeItem} links={docs.links} showDescription cssVariables={{ '--tree-item-level': "The depth of the item within the tree. Useful to calculate indentation." }} />

TreeItemContent

TreeSection

TreeHeader

<TreeHeader> labels the section within a Tree. It accepts all DOM attributes.

TreeLoadMoreItem

<PropTable component={docs.exports.TreeLoadMoreItem} links={docs.links} showDescription cssVariables={{ '--tree-item-level': "The depth of the item within the tree. Useful to calculate indentation." }} />