Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(TreeView): add support for Backspace to collapse a folder #2504

Merged
merged 9 commits into from
Nov 2, 2022
5 changes: 5 additions & 0 deletions .changeset/fast-shoes-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

Add support for Backspace to collapse group in TreeView
joshblack marked this conversation as resolved.
Show resolved Hide resolved
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"@github/combobox-nav": "^2.1.5",
"@github/markdown-toolbar-element": "^2.1.0",
"@github/paste-markdown": "^1.4.0",
"@primer/behaviors": "1.3.0",
"@primer/behaviors": "1.3.1-rc.8de3bf8",
joshblack marked this conversation as resolved.
Show resolved Hide resolved
"@primer/octicons-react": "^17.7.0",
"@primer/primitives": "7.10.0",
"@react-aria/ssr": "^3.1.0",
Expand Down
64 changes: 64 additions & 0 deletions src/TreeView/TreeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,64 @@ describe('Keyboard interactions', () => {
})
})

describe('Backspace', () => {
it('should move focus to the parent item', () => {
const {getByRole} = renderWithTheme(
<TreeView aria-label="Test tree">
<TreeView.Item defaultExpanded>
Parent
<TreeView.SubTree>
<TreeView.Item>Child</TreeView.Item>
</TreeView.SubTree>
</TreeView.Item>
</TreeView>
)

const parentItem = getByRole('treeitem', {name: 'Parent'})
const child = getByRole('treeitem', {name: 'Child'})
child.focus()

// Press Backspace
fireEvent.keyDown(document.activeElement || document.body, {key: 'Backspace'})

expect(parentItem).toHaveFocus()
})

it('should not collapse an expanded item', () => {
const {getByRole, queryByRole} = renderWithTheme(
<TreeView aria-label="Test tree">
<TreeView.Item defaultExpanded>
Parent
<TreeView.SubTree>
<TreeView.Item>Child</TreeView.Item>
</TreeView.SubTree>
</TreeView.Item>
</TreeView>
)

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

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

// Subtree should be visible
expect(subtree).toBeVisible()

// Focus first item
parentItem.focus()

// Press Backspace
fireEvent.keyDown(document.activeElement || document.body, {key: 'Backspace'})

// aria-expanded should stay set as true
expect(parentItem).toHaveAttribute('aria-expanded', 'true')

// Parent item should still be focused
expect(parentItem).toHaveFocus()
})
})

describe('Home', () => {
it('moves focus to first visible item', () => {
const {getByRole} = renderWithTheme(
Expand Down Expand Up @@ -1110,6 +1168,12 @@ describe('Asyncronous loading', () => {

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

// Press Backspace
fireEvent.keyDown(document.activeElement || document.body, {key: 'Backspace'})

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

it('should remove `aria-expanded` if no content is loaded in', async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ const ErrorDialog: React.FC<TreeViewErrorDialogProps> = ({title = 'Error', child
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
onKeyDown={event => {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter'].includes(event.key)) {
if (['Backspace', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter'].includes(event.key)) {
// Prevent keyboard events from bubbling up to the TreeView
// and interfering with keyboard navigation
event.stopPropagation()
Expand Down
12 changes: 9 additions & 3 deletions src/TreeView/useRovingTabIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function useRovingTabIndex({containerRef}: {containerRef: React.RefObject
// TODO: Initialize focus to the aria-current item if it exists
useFocusZone({
containerRef,
bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd,
bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd | FocusKeys.Backspace,
preventScroll: true,
getNextFocusable: (direction, from, event) => {
if (!(from instanceof HTMLElement)) return
Expand All @@ -30,20 +30,26 @@ export function getNextFocusableElement(activeElement: HTMLElement, event: Keybo
// Close node; don't change focus
return

// Focus parent element
case 'open Backspace':
return getParentElement(activeElement)

case 'closed ArrowRight':
// Open node; don't change focus
return

// Focus parent element
case 'closed ArrowLeft':
// Focus parent element
case 'closed Backspace':
return getParentElement(activeElement)

case 'end ArrowRight':
// Do nothing
return

// Focus parent element
case 'end ArrowLeft':
// Focus parent element
case 'end Backspace':
return getParentElement(activeElement)
joshblack marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down