Skip to content

Commit

Permalink
TreeView: Add indication of empty directory (#5168)
Browse files Browse the repository at this point in the history
* Add indication of empty directory

* clean up

* Add changeset

* Update `TreeView` test

* Fix story

* Redo conditional

* Fix test

* Add test, fix condition
  • Loading branch information
TylerJDev authored Nov 4, 2024
1 parent 32ea032 commit b9749d4
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-shirts-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

TreeView: Adds indication of no nodes in a tree item, allows for `aria-expanded even if the item is empty.
3 changes: 2 additions & 1 deletion packages/react/src/TreeView/TreeView.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ AsyncError.args = {
}

export const EmptyDirectories: StoryFn = () => {
const [state, setState] = React.useState<SubTreeState>('loading')
const [state, setState] = React.useState<SubTreeState>('initial')
const timeoutId = React.useRef<ReturnType<typeof setTimeout> | null>(null)

React.useEffect(() => {
Expand All @@ -597,6 +597,7 @@ export const EmptyDirectories: StoryFn = () => {
<TreeView.Item
id="src"
onExpandedChange={expanded => {
setState('loading')
if (expanded) {
timeoutId.current = setTimeout(() => {
setState('done')
Expand Down
46 changes: 43 additions & 3 deletions packages/react/src/TreeView/TreeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ describe('Markup', () => {
expect(treeitem).not.toHaveAttribute('aria-expanded')

await user.click(getByText(/Item 2/))
expect(treeitem).not.toHaveAttribute('aria-expanded')
expect(treeitem).toHaveAttribute('aria-expanded', 'true')
})

it('should render with containIntrinsicSize', () => {
Expand Down Expand Up @@ -1537,7 +1537,7 @@ describe('Asyncronous loading', () => {
expect(parentItem).toHaveAttribute('aria-expanded', 'true')
})

it('should remove `aria-expanded` if no content is loaded in', async () => {
it('should update `aria-expanded` if no content is loaded in', async () => {
function Example() {
const [state, setState] = React.useState<SubTreeState>('loading')
const timeoutId = React.useRef<ReturnType<typeof setTimeout> | null>(null)
Expand Down Expand Up @@ -1584,6 +1584,46 @@ describe('Asyncronous loading', () => {
jest.runAllTimers()
})

expect(treeitem).not.toHaveAttribute('aria-expanded')
expect(treeitem).toHaveAttribute('aria-expanded', 'true')
expect(getByLabelText('No items found')).toBeInTheDocument()
})

it('should have `aria-expanded` when directory is empty', async () => {
const {getByRole} = renderWithTheme(
<TreeView aria-label="Files changed">
<TreeView.Item id="src" defaultExpanded>
<TreeView.LeadingVisual>
<TreeView.DirectoryIcon />
</TreeView.LeadingVisual>
Parent
<TreeView.SubTree>
<TreeView.Item id="src/Avatar.tsx">child</TreeView.Item>
<TreeView.Item id="src/Button.tsx" current>
child current
</TreeView.Item>
<TreeView.Item id="src/Box.tsx">
empty child
<TreeView.SubTree />
</TreeView.Item>
</TreeView.SubTree>
</TreeView.Item>
</TreeView>,
)

const parentItem = getByRole('treeitem', {name: 'Parent'})

// Parent item should be expanded
expect(parentItem).toHaveAttribute('aria-expanded', 'true')

// Current child should not have `aria-expanded`
expect(getByRole('treeitem', {name: 'child current'})).not.toHaveAttribute('aria-expanded')

// Empty child should not have `aria-expanded` when closed
expect(getByRole('treeitem', {name: 'empty child'})).not.toHaveAttribute('aria-expanded')

fireEvent.click(getByRole('treeitem', {name: 'empty child'}))

// Empty child should have `aria-expanded` when opened
expect(getByRole('treeitem', {name: 'empty child'})).toHaveAttribute('aria-expanded')
})
})
15 changes: 12 additions & 3 deletions packages/react/src/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export type TreeViewItemProps = {
containIntrinsicSize?: string
current?: boolean
defaultExpanded?: boolean
expanded?: boolean
expanded?: boolean | null
onExpandedChange?: (expanded: boolean) => void
onSelect?: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => void
className?: string
Expand Down Expand Up @@ -401,7 +401,7 @@ const Item = React.forwardRef<HTMLElement, TreeViewItemProps>(
// If defaultExpanded is not provided, we default to false unless the item
// is the current item, in which case we default to true.
defaultValue: () => expandedStateCache.current?.get(itemId) ?? defaultExpanded ?? isCurrentItem,
value: expanded,
value: expanded === null ? false : expanded,
onChange: onExpandedChange,
})
const {level} = React.useContext(ItemContext)
Expand Down Expand Up @@ -482,7 +482,7 @@ const Item = React.forwardRef<HTMLElement, TreeViewItemProps>(
aria-labelledby={ariaLabel ? undefined : ariaLabelledby || labelId}
aria-describedby={`${leadingVisualId} ${trailingVisualId}`}
aria-level={level}
aria-expanded={isSubTreeEmpty ? undefined : isExpanded}
aria-expanded={(isSubTreeEmpty && (!isExpanded || !hasSubTree)) || expanded === null ? undefined : isExpanded}
aria-current={isCurrentItem ? 'true' : undefined}
aria-selected={isFocused ? 'true' : 'false'}
data-has-leading-action={slots.leadingAction ? true : undefined}
Expand Down Expand Up @@ -697,6 +697,7 @@ const SubTree: React.FC<TreeViewSubTreeProps> = ({count, state, children}) => {
ref={ref}
>
{state === 'loading' ? <LoadingItem ref={loadingItemRef} count={count} /> : children}
{isSubTreeEmpty && state !== 'loading' ? <EmptyItem /> : null}
</ul>
)
}
Expand Down Expand Up @@ -785,6 +786,14 @@ const LoadingItem = React.forwardRef<HTMLElement, LoadingItemProps>(({count}, re
)
})

const EmptyItem = React.forwardRef<HTMLElement>((props, ref) => {
return (
<Item expanded={null} id={useId()} ref={ref}>
<Text sx={{color: 'fg.muted'}}>No items found</Text>
</Item>
)
})

function useSubTree(children: React.ReactNode) {
return React.useMemo(() => {
const subTree = React.Children.toArray(children).find(
Expand Down

0 comments on commit b9749d4

Please sign in to comment.