Skip to content

Commit aca96c0

Browse files
authored
TreeView: Create ErrorDialog component (#2452)
* Create TreeView.ErrorDialog component * Fix icon size * Add error dialog tests * Update docs * Create tough-lobsters-greet.md * Remove settimeout from test
1 parent c367b44 commit aca96c0

File tree

5 files changed

+217
-44
lines changed

5 files changed

+217
-44
lines changed

.changeset/tough-lobsters-greet.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+
TreeView: Add `TreeView.ErrorDialog` component

docs/content/TreeView.mdx

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,42 @@ See [Storybook](https://primer.style/react/storybook?path=/story/components-tree
272272
{/* <PropsTableSxRow /> */}
273273
</PropsTable>
274274

275+
### TreeView.LeadingVisual
276+
277+
<PropsTable>
278+
<PropsTableRow
279+
name="children"
280+
type={`| React.ReactNode
281+
| (props: {isExpanded: boolean}) => React.ReactNode`}
282+
/>
283+
<PropsTableRow
284+
name="label"
285+
type="string"
286+
description="Provide an accessible label for the leading visual. This is not necessary for decorative visuals"
287+
/>
288+
{/* <PropsTableSxRow /> */}
289+
</PropsTable>
290+
291+
### TreeView.TrailingVisual
292+
293+
<PropsTable>
294+
<PropsTableRow
295+
name="children"
296+
type={`| React.ReactNode
297+
| (props: {isExpanded: boolean}) => React.ReactNode`}
298+
/>
299+
<PropsTableRow
300+
name="label"
301+
type="string"
302+
description="Provide an accessible label for the trailing visual. This is not necessary for decorative visuals"
303+
/>
304+
{/* <PropsTableSxRow /> */}
305+
</PropsTable>
306+
307+
### TreeView.DirectoryIcon
308+
309+
<PropsTable>{/* <PropsTableSxRow /> */}</PropsTable>
310+
275311
### TreeView.SubTree
276312

277313
<PropsTable>
@@ -315,42 +351,30 @@ See [Storybook](https://primer.style/react/storybook?path=/story/components-tree
315351
<PropsTableSxRow />
316352
</PropsTable>
317353

318-
### TreeView.LeadingVisual
354+
### TreeView.ErrorDialog
319355

320356
<PropsTable>
321357
<PropsTableRow
322358
name="children"
323-
type={`| React.ReactNode
324-
| (props: {isExpanded: boolean}) => React.ReactNode`}
359+
type="React.ReactNode"
360+
required
361+
description="The content of the dialog. This is usually a message explaining the error."
325362
/>
326363
<PropsTableRow
327-
name="label"
364+
name="title"
328365
type="string"
329-
description="Provide an accessible label for the leading visual. This is not necessary for decorative visuals"
330-
/>
331-
{/* <PropsTableSxRow /> */}
332-
</PropsTable>
333-
334-
### TreeView.TrailingVisual
335-
336-
<PropsTable>
337-
<PropsTableRow
338-
name="children"
339-
type={`| React.ReactNode
340-
| (props: {isExpanded: boolean}) => React.ReactNode`}
366+
defaultValue="'Error'"
367+
description="The title of the dialog. This is usually a short description of the error."
341368
/>
342369
<PropsTableRow
343-
name="label"
344-
type="string"
345-
description="Provide an accessible label for the trailing visual. This is not necessary for decorative visuals"
370+
name="onRetry"
371+
type="() => void"
372+
description="Event handler called when the user clicks the retry button."
346373
/>
374+
<PropsTableRow name="onDismiss" type="() => void" description="Event handler called when the dialog is dismissed." />
347375
{/* <PropsTableSxRow /> */}
348376
</PropsTable>
349377

350-
### TreeView.DirectoryIcon
351-
352-
<PropsTable>{/* <PropsTableSxRow /> */}</PropsTable>
353-
354378
## Status
355379

356380
<ComponentChecklist

src/TreeView/TreeView.stories.tsx

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {ActionList} from '../ActionList'
55
import {ActionMenu} from '../ActionMenu'
66
import Box from '../Box'
77
import {Button} from '../Button'
8-
import {ConfirmationDialog} from '../Dialog/ConfirmationDialog'
98
import StyledOcticon from '../StyledOcticon'
109
import {SubTreeState, TreeView} from './TreeView'
1110

@@ -421,6 +420,12 @@ export const AsyncSuccess: Story = args => {
421420
<Box sx={{p: 3}}>
422421
<nav aria-label="Files">
423422
<TreeView aria-label="Files">
423+
<TreeView.Item>
424+
<TreeView.LeadingVisual>
425+
<FileIcon />
426+
</TreeView.LeadingVisual>
427+
Some file
428+
</TreeView.Item>
424429
<TreeView.Item
425430
onExpandedChange={async isExpanded => {
426431
if (asyncItems.length === 0 && isExpanded) {
@@ -449,6 +454,12 @@ export const AsyncSuccess: Story = args => {
449454
))}
450455
</TreeView.SubTree>
451456
</TreeView.Item>
457+
<TreeView.Item>
458+
<TreeView.LeadingVisual>
459+
<FileIcon />
460+
</TreeView.LeadingVisual>
461+
Another file
462+
</TreeView.Item>
452463
</TreeView>
453464
</nav>
454465
</Box>
@@ -568,7 +579,6 @@ async function alwaysFails(responseTime: number) {
568579

569580
export const AsyncError: Story = args => {
570581
const [isLoading, setIsLoading] = React.useState(false)
571-
const [isExpanded, setIsExpanded] = React.useState(false)
572582
const [asyncItems, setAsyncItems] = React.useState<string[]>([])
573583
const [error, setError] = React.useState<Error | null>(null)
574584

@@ -602,11 +612,14 @@ export const AsyncError: Story = args => {
602612
<Box sx={{p: 3}}>
603613
<nav aria-label="Files">
604614
<TreeView aria-label="Files">
615+
<TreeView.Item>
616+
<TreeView.LeadingVisual>
617+
<FileIcon />
618+
</TreeView.LeadingVisual>
619+
Some file
620+
</TreeView.Item>
605621
<TreeView.Item
606-
expanded={isExpanded}
607622
onExpandedChange={isExpanded => {
608-
setIsExpanded(isExpanded)
609-
610623
if (isExpanded) {
611624
loadItems()
612625
}
@@ -618,21 +631,17 @@ export const AsyncError: Story = args => {
618631
Directory with async items
619632
<TreeView.SubTree state={state}>
620633
{error ? (
621-
<ConfirmationDialog
622-
title="Error"
623-
onClose={gesture => {
634+
<TreeView.ErrorDialog
635+
onRetry={() => {
636+
setError(null)
637+
loadItems()
638+
}}
639+
onDismiss={() => {
624640
setError(null)
625-
626-
if (gesture === 'confirm') {
627-
loadItems()
628-
} else {
629-
setIsExpanded(false)
630-
}
631641
}}
632-
confirmButtonContent="Retry"
633642
>
634643
{error.message}
635-
</ConfirmationDialog>
644+
</TreeView.ErrorDialog>
636645
) : null}
637646
{asyncItems.map(item => (
638647
<TreeView.Item key={item}>
@@ -644,6 +653,12 @@ export const AsyncError: Story = args => {
644653
))}
645654
</TreeView.SubTree>
646655
</TreeView.Item>
656+
<TreeView.Item>
657+
<TreeView.LeadingVisual>
658+
<FileIcon />
659+
</TreeView.LeadingVisual>
660+
Another file
661+
</TreeView.Item>
647662
</TreeView>
648663
</nav>
649664
</Box>

src/TreeView/TreeView.test.tsx

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {fireEvent, render} from '@testing-library/react'
1+
import {fireEvent, render, waitFor} from '@testing-library/react'
22
import React from 'react'
33
import {ThemeProvider} from '../ThemeProvider'
44
import {SubTreeState, TreeView} from './TreeView'
@@ -1121,4 +1121,80 @@ describe('Asyncronous loading', () => {
11211121
expect(firstChild).toHaveFocus()
11221122
})
11231123
})
1124+
1125+
it.only('moves focus to parent item after closing error dialog', async () => {
1126+
function TestTree() {
1127+
const [error, setError] = React.useState('Test error')
1128+
1129+
return (
1130+
<TreeView aria-label="Test tree">
1131+
<TreeView.Item defaultExpanded>
1132+
Parent
1133+
<TreeView.SubTree>
1134+
{error ? (
1135+
<TreeView.ErrorDialog
1136+
onRetry={() => {
1137+
setError('')
1138+
}}
1139+
onDismiss={() => setError('')}
1140+
>
1141+
{error}
1142+
</TreeView.ErrorDialog>
1143+
) : null}
1144+
</TreeView.SubTree>
1145+
</TreeView.Item>
1146+
</TreeView>
1147+
)
1148+
}
1149+
1150+
const {getByRole} = renderWithTheme(<TestTree />)
1151+
1152+
const dialog = getByRole('alertdialog')
1153+
const parentItem = getByRole('treeitem', {name: 'Parent'})
1154+
1155+
// Parent item should not be focused
1156+
expect(parentItem).not.toHaveFocus()
1157+
1158+
// Dialog should be visible
1159+
expect(dialog).toBeVisible()
1160+
1161+
// Press esc to close error dialog
1162+
fireEvent.keyDown(document.activeElement || document.body, {key: 'Escape'})
1163+
1164+
// Dialog should not be visible
1165+
expect(dialog).not.toBeVisible()
1166+
1167+
await waitFor(() => {
1168+
// Parent item should be focused
1169+
expect(parentItem).toHaveFocus()
1170+
})
1171+
})
1172+
1173+
it('ignores arrow keys when error dialog is open', async () => {
1174+
const {getByRole} = renderWithTheme(
1175+
<TreeView aria-label="Test tree">
1176+
<TreeView.Item defaultExpanded>
1177+
Parent
1178+
<TreeView.SubTree>
1179+
<TreeView.ErrorDialog>Opps</TreeView.ErrorDialog>
1180+
<TreeView.Item>Child</TreeView.Item>
1181+
</TreeView.SubTree>
1182+
</TreeView.Item>
1183+
</TreeView>
1184+
)
1185+
1186+
const parentItem = getByRole('treeitem', {name: 'Parent'})
1187+
1188+
// Parent item should be expanded
1189+
expect(parentItem).toHaveAttribute('aria-expanded', 'true')
1190+
1191+
// Focus first item
1192+
parentItem.focus()
1193+
1194+
// Press ←
1195+
fireEvent.keyDown(document.activeElement || document.body, {key: 'ArrowLeft'})
1196+
1197+
// Parent item should still be expanded
1198+
expect(parentItem).toHaveAttribute('aria-expanded', 'true')
1199+
})
11241200
})

0 commit comments

Comments
 (0)