Skip to content

Commit 6cd50a5

Browse files
authored
Implement Treeview markup (#2305)
* Add TreeView docs * Update TreeView props * Scaffold treeview markup * Add TreeView to drafts * Add comment * Track item levels * Add TreeView stories * Update TreeView docs * Update TreeView markup * Create curly-birds-argue.md * Fix examples * Update src/TreeView/TreeView.tsx * Update docs/content/TreeView.mdx * Add some tests
1 parent 027e44a commit 6cd50a5

File tree

7 files changed

+534
-3
lines changed

7 files changed

+534
-3
lines changed

.changeset/curly-birds-argue.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": patch
3+
---
4+
5+
Add draft TreeView component

docs/content/TreeView.mdx

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
title: TreeView
3+
componentId: tree_view
4+
status: Draft
5+
description: A hierarchical list of items where nested items can be expanded and collapsed.
6+
---
7+
8+
## Examples
9+
10+
### File tree navigation without directory links
11+
12+
```jsx live drafts
13+
<nav aria-label="File navigation">
14+
<TreeView aria-label="File navigation">
15+
<TreeView.Item>
16+
src
17+
<TreeView.SubTree>
18+
<TreeView.LinkItem href="#">Avatar.tsx</TreeView.LinkItem>
19+
<TreeView.Item>
20+
Button
21+
<TreeView.SubTree>
22+
<TreeView.LinkItem href="#">Button.tsx</TreeView.LinkItem>
23+
<TreeView.LinkItem href="#">Button.test.tsx</TreeView.LinkItem>
24+
</TreeView.SubTree>
25+
</TreeView.Item>
26+
</TreeView.SubTree>
27+
</TreeView.Item>
28+
<TreeView.Item>
29+
public
30+
<TreeView.SubTree>
31+
<TreeView.LinkItem href="#">index.html</TreeView.LinkItem>
32+
<TreeView.LinkItem href="#">favicon.ico</TreeView.LinkItem>
33+
</TreeView.SubTree>
34+
</TreeView.Item>
35+
<TreeView.LinkItem href="#">package.json</TreeView.LinkItem>
36+
</TreeView>
37+
</nav>
38+
```
39+
40+
### File tree navigation with directory links
41+
42+
```jsx live drafts
43+
<nav aria-label="File navigation">
44+
<TreeView aria-label="File navigation">
45+
<TreeView.LinkItem href="#">
46+
src
47+
<TreeView.SubTree>
48+
<TreeView.LinkItem href="#">Avatar.tsx</TreeView.LinkItem>
49+
<TreeView.LinkItem href="#">
50+
Button
51+
<TreeView.SubTree>
52+
<TreeView.LinkItem href="#">Button.tsx</TreeView.LinkItem>
53+
<TreeView.LinkItem href="#">Button.test.tsx</TreeView.LinkItem>
54+
</TreeView.SubTree>
55+
</TreeView.LinkItem>
56+
</TreeView.SubTree>
57+
</TreeView.LinkItem>
58+
<TreeView.LinkItem href="#">
59+
public
60+
<TreeView.SubTree>
61+
<TreeView.LinkItem href="#">index.html</TreeView.LinkItem>
62+
<TreeView.LinkItem href="#">favicon.ico</TreeView.LinkItem>
63+
</TreeView.SubTree>
64+
</TreeView.LinkItem>
65+
<TreeView.LinkItem href="#">package.json</TreeView.LinkItem>
66+
</TreeView>
67+
</nav>
68+
```
69+
70+
## Props
71+
72+
### TreeView
73+
74+
<PropsTable>
75+
<PropsTableRow name="children" type="React.ReactNode" required />
76+
{/* <PropsTableSxRow /> */}
77+
</PropsTable>
78+
79+
### TreeView.Item
80+
81+
<PropsTable>
82+
<PropsTableRow name="children" type="React.ReactNode" required />
83+
<PropsTableRow
84+
name="onSelect"
85+
type="(event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => void"
86+
/>
87+
<PropsTableRow name="onToggle" type="(isExpanded: boolean) => void" />
88+
{/* <PropsTableSxRow /> */}
89+
</PropsTable>
90+
91+
### TreeView.LinkItem
92+
93+
<PropsTable>
94+
<PropsTableRow name="children" type="React.ReactNode" required />
95+
<PropsTableRow
96+
name="href"
97+
type="string"
98+
description={
99+
<>
100+
The URL that the item navigates to. <InlineCode>href</InlineCode> is passed to the underlying{' '}
101+
<InlineCode>&lt;a&gt;</InlineCode> element. If <InlineCode>as</InlineCode> is specified, the component may need
102+
different props. If the item contains a sub-nav, the item is rendered as a{' '}
103+
<InlineCode>&lt;button&gt;</InlineCode> and <InlineCode>href</InlineCode> is ignored.
104+
</>
105+
}
106+
/>
107+
<PropsTableRow
108+
name="onSelect"
109+
type="(event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => void"
110+
/>
111+
<PropsTableRow name="onToggle" type="(isExpanded: boolean) => void" />
112+
{/* <PropsTableSxRow /> */}
113+
</PropsTable>
114+
115+
### TreeView.SubTree
116+
117+
<PropsTable>
118+
<PropsTableRow name="children" type="React.ReactNode" />
119+
{/* <PropsTableSxRow /> */}
120+
</PropsTable>
121+
122+
<!-- TODO: Add leading and trailing visuals -->
123+
124+
<!-- ### TreeView.LeadingVisual
125+
126+
<PropsTable>
127+
<PropsTableRow
128+
name="children"
129+
type={`| React.ReactNode
130+
| (props: {isExpanded: boolean}) => React.ReactNode`}
131+
/>
132+
<PropsTableSxRow />
133+
</PropsTable>
134+
135+
### TreeView.TrailingVisual
136+
137+
<PropsTable>
138+
<PropsTableRow
139+
name="children"
140+
type={`| React.ReactNode
141+
| (props: {isExpanded: boolean}) => React.ReactNode`}
142+
/>
143+
<PropsTableSxRow />
144+
</PropsTable>
145+
146+
### TreeView.FolderIcon
147+
148+
<PropsTable>
149+
<PropsTableSxRow />
150+
</PropsTable> -->
151+
152+
<!-- TODO: Add components to support async behavior (e.g. LoadingItem) -->
153+
154+
## Status
155+
156+
<ComponentChecklist
157+
items={{
158+
propsDocumented: true,
159+
noUnnecessaryDeps: false,
160+
adaptsToThemes: false,
161+
adaptsToScreenSizes: false,
162+
fullTestCoverage: false,
163+
usedInProduction: false,
164+
usageExamplesDocumented: false,
165+
hasStorybookStories: false,
166+
designReviewed: false,
167+
a11yReviewed: false,
168+
stableApi: false,
169+
addressedApiFeedback: false,
170+
hasDesignGuidelines: false,
171+
hasFigmaComponent: false
172+
}}
173+
/>

src/TreeView/TreeView.stories.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {Meta, Story} from '@storybook/react'
2+
import {TreeView} from './TreeView'
3+
import React from 'react'
4+
import Box from '../Box'
5+
6+
const meta: Meta = {
7+
title: 'Composite components/TreeView',
8+
component: TreeView,
9+
parameters: {
10+
layout: 'fullscreen'
11+
}
12+
}
13+
14+
export const FileTreeWithDirectoryLinks: Story = () => (
15+
<Box p={3}>
16+
<nav aria-label="File navigation">
17+
<TreeView aria-label="File navigation">
18+
<TreeView.LinkItem href="#">
19+
src
20+
<TreeView.SubTree>
21+
<TreeView.LinkItem href="#">Avatar.tsx</TreeView.LinkItem>
22+
<TreeView.LinkItem href="#">
23+
Button
24+
<TreeView.SubTree>
25+
<TreeView.LinkItem href="#">Button.tsx</TreeView.LinkItem>
26+
<TreeView.LinkItem href="#">Button.test.tsx</TreeView.LinkItem>
27+
</TreeView.SubTree>
28+
</TreeView.LinkItem>
29+
</TreeView.SubTree>
30+
</TreeView.LinkItem>
31+
<TreeView.LinkItem
32+
href="#"
33+
// eslint-disable-next-line no-console
34+
onToggle={isExpanded => console.log(`${isExpanded ? 'Expanded' : 'Collapsed'} "public" folder `)}
35+
>
36+
public
37+
<TreeView.SubTree>
38+
<TreeView.LinkItem href="#">index.html</TreeView.LinkItem>
39+
<TreeView.LinkItem href="#">favicon.ico</TreeView.LinkItem>
40+
</TreeView.SubTree>
41+
</TreeView.LinkItem>
42+
<TreeView.LinkItem href="#">package.json</TreeView.LinkItem>
43+
</TreeView>
44+
</nav>
45+
</Box>
46+
)
47+
48+
export const FileTreeWithoutDirectoryLinks: Story = () => (
49+
<Box p={3}>
50+
<nav aria-label="File navigation">
51+
<TreeView aria-label="File navigation">
52+
<TreeView.Item>
53+
src
54+
<TreeView.SubTree>
55+
<TreeView.LinkItem href="#">Avatar.tsx</TreeView.LinkItem>
56+
<TreeView.Item>
57+
Button
58+
<TreeView.SubTree>
59+
<TreeView.LinkItem href="#">Button.tsx</TreeView.LinkItem>
60+
<TreeView.LinkItem href="#">Button.test.tsx</TreeView.LinkItem>
61+
</TreeView.SubTree>
62+
</TreeView.Item>
63+
</TreeView.SubTree>
64+
</TreeView.Item>
65+
<TreeView.Item
66+
// eslint-disable-next-line no-console
67+
onToggle={isExpanded => console.log(`${isExpanded ? 'Expanded' : 'Collapsed'} "public" folder `)}
68+
>
69+
public
70+
<TreeView.SubTree>
71+
<TreeView.LinkItem href="#">index.html</TreeView.LinkItem>
72+
<TreeView.LinkItem href="#">favicon.ico</TreeView.LinkItem>
73+
</TreeView.SubTree>
74+
</TreeView.Item>
75+
<TreeView.LinkItem href="#">package.json</TreeView.LinkItem>
76+
</TreeView>
77+
</nav>
78+
</Box>
79+
)
80+
81+
export default meta

src/TreeView/TreeView.test.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {render} from '@testing-library/react'
2+
import React from 'react'
3+
import {TreeView} from './TreeView'
4+
5+
it('uses tree role', () => {
6+
const {queryByRole} = render(
7+
<TreeView aria-label="Test tree">
8+
<TreeView.Item>Item 1</TreeView.Item>
9+
<TreeView.Item>Item 2</TreeView.Item>
10+
<TreeView.Item>Item 3</TreeView.Item>
11+
</TreeView>
12+
)
13+
14+
const root = queryByRole('tree')
15+
16+
expect(root).toHaveAccessibleName('Test tree')
17+
})
18+
19+
it('uses treeitem role', () => {
20+
const {queryAllByRole} = render(
21+
<TreeView aria-label="Test tree">
22+
<TreeView.Item>Item 1</TreeView.Item>
23+
<TreeView.Item>Item 2</TreeView.Item>
24+
<TreeView.Item>Item 3</TreeView.Item>
25+
</TreeView>
26+
)
27+
28+
const items = queryAllByRole('treeitem')
29+
30+
expect(items).toHaveLength(3)
31+
})
32+
33+
it('hides subtrees by default', () => {
34+
const {queryByRole} = render(
35+
<TreeView aria-label="Test tree">
36+
<TreeView.Item>
37+
Parent
38+
<TreeView.SubTree>
39+
<TreeView.Item>Child</TreeView.Item>
40+
</TreeView.SubTree>
41+
</TreeView.Item>
42+
</TreeView>
43+
)
44+
45+
const parentItem = queryByRole('treeitem', {name: 'Parent'})
46+
const subtree = queryByRole('group')
47+
48+
expect(parentItem).toHaveAttribute('aria-expanded', 'false')
49+
expect(subtree).toBeNull()
50+
})

0 commit comments

Comments
 (0)