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
---

TreeView: Add support for Backspace to move focus to parent item
14 changes: 7 additions & 7 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",
"@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 @@ -719,7 +719,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
5 changes: 4 additions & 1 deletion 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 Down Expand Up @@ -57,6 +57,9 @@ export function getNextFocusableElement(activeElement: HTMLElement, event: Keybo
// Focus next visible element
return getVisibleElement(activeElement, 'next')

case 'Backspace':
return getParentElement(activeElement)

case 'Home':
// Focus first visible element
return getFirstElement(activeElement)
Expand Down