diff --git a/.changeset/twelve-beers-raise.md b/.changeset/twelve-beers-raise.md
new file mode 100644
index 00000000000..792d6e8fca7
--- /dev/null
+++ b/.changeset/twelve-beers-raise.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": patch
+---
+
+TreeView: Add support for leading and trailing visuals
diff --git a/docs/content/TreeView.mdx b/docs/content/TreeView.mdx
index d39e08369d6..931faf43bb1 100644
--- a/docs/content/TreeView.mdx
+++ b/docs/content/TreeView.mdx
@@ -10,63 +10,98 @@ description: A hierarchical list of items where nested items can be expanded and
### File tree navigation without directory links
```jsx live drafts
-
-
-
- src
-
- Avatar.tsx
-
- Button
-
-
- Button.tsx
-
- Button.test.tsx
-
-
-
-
-
- public
-
- index.html
- favicon.ico
-
-
- package.json
-
-
+
+
+
+
+
+
+
+ src
+
+
+
+
+
+ Avatar.tsx
+
+
+
+
+
+
+
+
+ Button.tsx
+
+
+
+
+
+
+
+
+
+
+ package.json
+
+
+
+
+
+
+
```
### File tree navigation with directory links
```jsx live drafts
-
-
-
- src
-
- Avatar.tsx
-
- Button
-
- Button.tsx
- Button.test.tsx
-
-
-
-
-
- public
-
- index.html
- favicon.ico
-
-
- package.json
-
-
+
+
+
+
+
+
+
+ src
+
+
+
+
+
+ Avatar.tsx
+
+
+
+
+
+ Button
+
+
+
+
+
+ Button.tsx
+
+
+
+
+
+ Button.test.tsx
+
+
+
+
+
+
+
+
+
+ package.json
+
+
+
+
```
### With controlled expanded state
@@ -76,7 +111,7 @@ function ControlledTreeView() {
const [expanded, setExpanded] = React.useState(false)
return (
-
+
setExpanded(!expanded)}>{expanded ? 'Collapse' : 'Expand'}
@@ -98,6 +133,54 @@ function ControlledTreeView() {
render( )
```
+### With stateful visuals
+
+To render stateful visuals, pass a render function to `TreeView.LeadingVisual` or `TreeView.TrailingVisual`. The function will be called with the `expanded` state of the item.
+
+```jsx live drafts
+
+
+
+
+
+ {({isExpanded}) => (isExpanded ? : )}
+
+ src
+
+ Avatar.tsx
+
+ Button.tsx
+
+
+
+
+
+
+```
+
+Since stateful directory icons are a common use case for TreeView, we provide a `TreeView.DirectoryIcon` component for convenience. The previous example can be rewritten as:
+
+```jsx live drafts
+
+
+
+
+
+
+
+ src
+
+ Avatar.tsx
+
+ Button.tsx
+
+
+
+
+
+
+```
+
## Props
### TreeView
@@ -190,9 +273,7 @@ render( )
{/* */}
-
-
-
+{/* */}
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 222db9c2f47..e580469b0a3 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"dependencies": {
"@primer/gatsby-theme-doctocat": "^4.0.0",
- "@primer/octicons-react": "^16.1.0",
+ "@primer/octicons-react": "^17.5.0",
"@primer/primitives": "4.1.0",
"@styled-system/prop-types": "^5.1.0",
"@styled-system/theme-get": "^5.1.0",
@@ -2792,6 +2792,17 @@
"react-dom": "^16.9.x"
}
},
+ "node_modules/@primer/gatsby-theme-doctocat/node_modules/@primer/octicons-react": {
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-16.3.1.tgz",
+ "integrity": "sha512-uzTs8/CvLiW1/47cgMRkIK9bKWpnw+UonCbgczXErwSSLqMDHfiiTpobW1trvRuoiMgLwsPo0l7kBBdKBnmq3g==",
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "react": ">=15"
+ }
+ },
"node_modules/@primer/gatsby-theme-doctocat/node_modules/find-up": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
@@ -2869,9 +2880,9 @@
}
},
"node_modules/@primer/octicons-react": {
- "version": "16.3.1",
- "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-16.3.1.tgz",
- "integrity": "sha512-uzTs8/CvLiW1/47cgMRkIK9bKWpnw+UonCbgczXErwSSLqMDHfiiTpobW1trvRuoiMgLwsPo0l7kBBdKBnmq3g==",
+ "version": "17.5.0",
+ "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-17.5.0.tgz",
+ "integrity": "sha512-7z/uwKn/3w+DHEMFynEfHLYPHMjFzvsL88plimWhXou1hD4lriCUTvp65uDvdpyLqKyq5luEupnQmU+RiBODog==",
"engines": {
"node": ">=8"
},
@@ -29245,6 +29256,11 @@
"worker-loader": "^3.0.2"
},
"dependencies": {
+ "@primer/octicons-react": {
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-16.3.1.tgz",
+ "integrity": "sha512-uzTs8/CvLiW1/47cgMRkIK9bKWpnw+UonCbgczXErwSSLqMDHfiiTpobW1trvRuoiMgLwsPo0l7kBBdKBnmq3g=="
+ },
"find-up": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
@@ -29291,9 +29307,9 @@
}
},
"@primer/octicons-react": {
- "version": "16.3.1",
- "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-16.3.1.tgz",
- "integrity": "sha512-uzTs8/CvLiW1/47cgMRkIK9bKWpnw+UonCbgczXErwSSLqMDHfiiTpobW1trvRuoiMgLwsPo0l7kBBdKBnmq3g=="
+ "version": "17.5.0",
+ "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-17.5.0.tgz",
+ "integrity": "sha512-7z/uwKn/3w+DHEMFynEfHLYPHMjFzvsL88plimWhXou1hD4lriCUTvp65uDvdpyLqKyq5luEupnQmU+RiBODog=="
},
"@primer/primitives": {
"version": "4.1.0",
diff --git a/docs/package.json b/docs/package.json
index 75ec2108aba..12909f35317 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -14,7 +14,7 @@
},
"dependencies": {
"@primer/gatsby-theme-doctocat": "^4.0.0",
- "@primer/octicons-react": "^16.1.0",
+ "@primer/octicons-react": "^17.5.0",
"@primer/primitives": "4.1.0",
"@styled-system/prop-types": "^5.1.0",
"@styled-system/theme-get": "^5.1.0",
diff --git a/src/TreeView/TreeView.stories.tsx b/src/TreeView/TreeView.stories.tsx
index cad45fa2a2c..bb7ede03117 100644
--- a/src/TreeView/TreeView.stories.tsx
+++ b/src/TreeView/TreeView.stories.tsx
@@ -1,7 +1,9 @@
-import {Meta, Story} from '@storybook/react'
-import {TreeView} from './TreeView'
import React from 'react'
+import {DiffAddedIcon, DiffModifiedIcon, DiffRemovedIcon, DiffRenamedIcon, FileIcon} from '@primer/octicons-react'
+import {Meta, Story} from '@storybook/react'
import Box from '../Box'
+import StyledOcticon from '../StyledOcticon'
+import {TreeView} from './TreeView'
import {Button} from '../Button'
import {ActionMenu} from '../ActionMenu'
import {ActionList} from '../ActionList'
@@ -15,20 +17,47 @@ const meta: Meta = {
}
export const FileTreeWithDirectoryLinks: Story = () => (
-
+
+
+
+
src
- Avatar.tsx
+
+
+
+
+ Avatar.tsx
+
+
+
+
Button
- Button.tsx
- Button.test.tsx
+
+
+
+
+ Button.tsx
+
+
+
+
+
+ Button.test.tsx
+
+
+
+
+
+ ReallyLongFileNameThatShouldBeTruncated.tsx
+
(
// eslint-disable-next-line no-console
onExpandedChange={isExpanded => console.log(`${isExpanded ? 'Expanded' : 'Collapsed'} "public" folder `)}
>
+
+
+
public
- index.html
- favicon.ico
+
+
+
+
+ index.html
+
+
+
+
+
+ favicon.ico
+
- package.json
+
+
+
+
+ package.json
+
@@ -50,35 +97,96 @@ export const FileTreeWithDirectoryLinks: Story = () => (
export const FileTreeWithoutDirectoryLinks: Story = () => {
return (
-
+
-
+
+
+
+
src
- Avatar.tsx
-
+
+
+
+
+ Avatar.tsx
+
+
+
+
+
+
+
+
Button
+
+
+
Button.tsx
+
+
+
+
+
+
+
+
+ Button.test.tsx
+
+
+
- Button.test.tsx
+
+
+
+
+ ReallyLongFileNameThatShouldBeTruncated.tsx
+
+
+
+
- console.log(`${isExpanded ? 'Expanded' : 'Collapsed'} "public" folder `)}
- >
+
+
+
+
public
- index.html
- favicon.ico
+
+
+
+
+ index.html
+
+
+
+
+
+
+
+
+ favicon.ico
+
+
+
+
- package.json
+
+
+
+
+ package.json
+
+
+
+
diff --git a/src/TreeView/TreeView.tsx b/src/TreeView/TreeView.tsx
index 7b41b2076d4..5aea8195772 100644
--- a/src/TreeView/TreeView.tsx
+++ b/src/TreeView/TreeView.tsx
@@ -1,11 +1,19 @@
-import {ChevronDownIcon, ChevronRightIcon} from '@primer/octicons-react'
+import {
+ ChevronDownIcon,
+ ChevronRightIcon,
+ FileDirectoryFillIcon,
+ FileDirectoryOpenFillIcon
+} from '@primer/octicons-react'
import {useSSRSafeId} from '@react-aria/ssr'
import React from 'react'
import styled from 'styled-components'
import Box from '../Box'
+import StyledOcticon from '../StyledOcticon'
import {useControllableState} from '../hooks/useControllableState'
import sx, {SxProp} from '../sx'
+import Text from '../Text'
import {Theme} from '../ThemeProvider'
+import createSlots from '../utils/create-slots'
import {useActiveDescendant} from './useActiveDescendant'
import {useTypeahead} from './useTypeahead'
@@ -86,6 +94,8 @@ export type TreeViewItemProps = {
onSelect?: (event: React.MouseEvent | KeyboardEvent) => void
}
+const {Slots, Slot} = createSlots(['LeadingVisual', 'TrailingVisual'])
+
const Item: React.FC = ({
current: isCurrentItem = false,
defaultExpanded = false,
@@ -266,10 +276,31 @@ const Item: React.FC = ({
display: 'flex',
alignItems: 'center',
height: '100%',
- px: 2
+ px: 2,
+ gap: 2
}}
>
- {childrenWithoutSubTree}
+
+ {slots => (
+ // QUESTION: How should leading and trailing visuals impact the aria-label?
+ <>
+ {slots.LeadingVisual}
+
+ {childrenWithoutSubTree}
+
+ {slots.TrailingVisual}
+ >
+ )}
+
{subTree}
@@ -374,11 +405,51 @@ function useSubTree(children: React.ReactNode) {
}, [children])
}
+// ----------------------------------------------------------------------------
+// TreeView.LeadingVisual and TreeView.TrailingVisual
+
+export type TreeViewVisualProps = {
+ children: React.ReactNode | ((props: {isExpanded: boolean}) => React.ReactNode)
+}
+
+const LeadingVisual: React.FC = props => {
+ const {isExpanded} = React.useContext(ItemContext)
+ const children = typeof props.children === 'function' ? props.children({isExpanded}) : props.children
+ return (
+
+ {children}
+
+ )
+}
+
+const TrailingVisual: React.FC = props => {
+ const {isExpanded} = React.useContext(ItemContext)
+ const children = typeof props.children === 'function' ? props.children({isExpanded}) : props.children
+ return (
+
+ {children}
+
+ )
+}
+
+// ----------------------------------------------------------------------------
+// TreeView.DirectoryIcon
+
+const DirectoryIcon = () => {
+ const {isExpanded} = React.useContext(ItemContext)
+ const icon = isExpanded ? FileDirectoryOpenFillIcon : FileDirectoryFillIcon
+ // TODO: Use correct color
+ return
+}
+
// ----------------------------------------------------------------------------
// Export
export const TreeView = Object.assign(Root, {
Item,
LinkItem,
- SubTree
+ SubTree,
+ LeadingVisual,
+ TrailingVisual,
+ DirectoryIcon
})