Skip to content
4 changes: 3 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/Tree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -448,15 +448,17 @@ function Example() {

## API


<Anatomy role="img" aria-label="Anatomy diagram of a Tree, consisting of multiple items. Each item in the tree contains a selection checkbox, an expand toggle button, and the name of the item." />

```tsx links={{Tree: '#tree', TreeItem: '#treeitem', TreeItemContent: '#treeitemcontent', TreeSection: '#treesection', TreeHeader: '#treeheader', Button: 'Button', Checkbox: 'Checkbox', TreeLoadMoreItem: '#treeloadmoreitem', SelectionIndicator: 'selection#animated-selectionindicator'}}
```tsx links={{Tree: '#tree', TreeItem: '#treeitem', TreeItemContent: '#treeitemcontent', TreeSection: '#treesection', TreeHeader: '#treeheader', Button: 'Button', Checkbox: 'Checkbox', TreeLoadMoreItem: '#treeloadmoreitem', Text: 'Text', SelectionIndicator: 'selection#animated-selectionindicator'}}
<Tree>
<TreeItem>
<TreeItemContent>
<Button slot="chevron" />
<Checkbox slot="selection" /> or <SelectionIndicator />
<Button slot="drag" />
<Text slot="description" />
</TreeItemContent>
<TreeItem>
{/* ... */}
Expand Down
5 changes: 2 additions & 3 deletions packages/react-aria-components/src/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import React, {
} from 'react';
import {SelectionIndicatorContext} from './SelectionIndicator';
import {SharedElementTransition} from './SharedElementTransition';
import {TextContext} from './Text';
import {TreeDropTargetDelegate} from './TreeDropTargetDelegate';
import {TreeState, useTreeState} from 'react-stately/useTreeState';
import {useCachedChildren} from 'react-aria/private/collections/useCachedChildren';
Expand Down Expand Up @@ -782,8 +783,6 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent(
let isDraggable =
dragState && !(dragState.isDisabled || dragState.selectionManager.isDisabled(item.key));

// TODO: remove this when we support description in tree row
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let {rowProps, gridCellProps, expandButtonProps, descriptionProps, ...states} = useTreeItem(
{
node: item,
Expand Down Expand Up @@ -1020,7 +1019,7 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent(
}
}
],
// TODO: support description in the tree row
[TextContext, {slots: {[DEFAULT_SLOT]: {}, description: descriptionProps}}],
// TODO: don't think I need to pass isExpanded to the button here since it can be sourced from the renderProps? Might be worthwhile passing it down?
[
ButtonContext,
Expand Down
30 changes: 30 additions & 0 deletions packages/react-aria-components/stories/Tree.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,36 @@ const TreeExampleStaticNoActionsRender = <T extends any>(args: TreeProps<T>): JS
</Tree>
);

export const WithDescriptions: TreeStory = args => (
<Tree {...args} aria-label="Files" className={styles.tree} selectionMode="single">
<TreeItem id="projects" textValue="Projects" className={styles['tree-item']}>
<TreeItemContent>
{({isExpanded}) => (
<>
<Button slot="chevron">{isExpanded ? '⏷' : '⏵'}</Button>
<div>
<Text>Projects</Text>
<Text slot="description" style={{display: 'block'}}>
Shared project files
</Text>
</div>
</>
)}
</TreeItemContent>
<TreeItem id="report" textValue="Report" className={styles['tree-item']}>
<TreeItemContent>
<div>
<Text>Report</Text>
<Text slot="description" style={{display: 'block'}}>
Last updated today
</Text>
</div>
</TreeItemContent>
</TreeItem>
</TreeItem>
</Tree>
);

export const TreeExampleStatic: StoryObj<typeof TreeExampleStaticRender> = {
render: args => <TreeExampleStaticRender {...args} />,
args: {
Expand Down
41 changes: 41 additions & 0 deletions packages/react-aria-components/test/Tree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,47 @@ describe('Tree', () => {
}
});

describe('descriptions', () => {
function DescriptionTree({
hideDescription = false,
itemAriaLabel
}: {
hideDescription?: boolean;
itemAriaLabel?: string;
}) {
return (
<Tree aria-label="Files">
<TreeItem id="projects" textValue="Projects" aria-label={itemAriaLabel}>
<TreeItemContent>
<Text>Projects</Text>
{!hideDescription && (
<Text slot="description" id="tree-desc">
Shared files
</Text>
)}
</TreeItemContent>
</TreeItem>
</Tree>
);
}

it('includes the description in the row name', () => {
let {getByRole} = render(<DescriptionTree />);
expect(getByRole('row')).toHaveAccessibleName('Projects Shared files');
});

it('includes the description with an explicit item aria-label', () => {
let {getByRole} = render(<DescriptionTree itemAriaLabel="Team projects" />);
expect(getByRole('row')).toHaveAccessibleName('Team projects Shared files');
});

it('keeps the row name when there is no description', () => {
let {getByRole} = render(<DescriptionTree hideDescription />);
expect(getByRole('row')).toHaveAccessibleName('Projects');
expect(getByRole('row')).not.toHaveAttribute('aria-labelledby');
});
});

it('should render a Tree with custom classes', () => {
let {getByRole, getAllByRole} = render(
<StaticTree treeProps={{className: 'test-tree'}} rowProps={{className: 'test-row'}} />
Expand Down
Loading