Skip to content

Commit

Permalink
feat(DataTable): add Table.ErrorDialog component (#3276)
Browse files Browse the repository at this point in the history
* feat(DataTable): add ErrorDialog component

* chore: add changeset

* chore: remove unused import

* Update generated/components.json

* chore: address eslint violations

---------

Co-authored-by: Josh Black <joshblack@users.noreply.github.com>
  • Loading branch information
joshblack and joshblack committed May 19, 2023
1 parent 29f797f commit 8abf268
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/brown-beds-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add experimental Table.ErrorDialog component
27 changes: 27 additions & 0 deletions generated/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,33 @@
"required": true
}
]
},
{
"name": "Table.ErrorDialog",
"props": [
{
"name": "children",
"required": true,
"type": "React.ReactNode",
"description": "The content of the dialog. This is usually a message explaining the error."
},
{
"name": "title",
"type": "string",
"defaultValue": "'Error'",
"description": "The title of the dialog. This is usually a short description of the error."
},
{
"name": "onRetry",
"type": "() => void",
"description": "Event handler called when the user clicks the retry button."
},
{
"name": "onDismiss",
"type": "() => void",
"description": "Event handler called when the dialog is dismissed."
}
]
}
]
},
Expand Down
27 changes: 27 additions & 0 deletions src/DataTable/DataTable.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,33 @@
"required": true
}
]
},
{
"name": "Table.ErrorDialog",
"props": [
{
"name": "children",
"required": true,
"type": "React.ReactNode",
"description": "The content of the dialog. This is usually a message explaining the error."
},
{
"name": "title",
"type": "string",
"defaultValue": "'Error'",
"description": "The title of the dialog. This is usually a short description of the error."
},
{
"name": "onRetry",
"type": "() => void",
"description": "Event handler called when the user clicks the retry button."
},
{
"name": "onDismiss",
"type": "() => void",
"description": "Event handler called when the dialog is dismissed."
}
]
}
]
}
54 changes: 53 additions & 1 deletion src/DataTable/DataTable.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import LabelGroup from '../LabelGroup'
import RelativeTime from '../RelativeTime'
import VisuallyHidden from '../_VisuallyHidden'
import {createColumnHelper} from './column'
import {repos} from './storybook/data'
import {fetchRepos, repos, useFlakeyQuery} from './storybook/data'

export default {
title: 'Components/DataTable/Features',
Expand Down Expand Up @@ -1524,3 +1524,55 @@ export const WithPagination = () => {
</Table.Container>
)
}

export const WithNetworkError = () => {
const pageSize = 10
const [pageIndex, setPageIndex] = React.useState(0)
const {error, loading, data} = useFlakeyQuery({
queryKey: ['repos', pageSize, pageIndex],
queryFn: () => {
return fetchRepos({
page: pageIndex,
perPage: pageSize,
})
},
})

return (
<Table.Container>
<Table.Title as="h2" id="repositories">
Repositories
</Table.Title>
<Table.Subtitle as="p" id="repositories-subtitle">
A subtitle could appear here to give extra context to the data.
</Table.Subtitle>
{loading || error ? <Table.Skeleton columns={columns} /> : null}
{error ? (
<Table.ErrorDialog
onDismiss={() => {
action('onDismiss')
}}
onRetry={() => {
action('onRetry')
}}
/>
) : null}
{data ? (
<DataTable
aria-labelledby="repositories"
aria-describedby="repositories-subtitle"
data={data}
columns={columns}
/>
) : null}
<Table.Pagination
aria-label="Pagination for Repositories"
pageSize={pageSize}
totalCount={repos.length}
onChange={({pageIndex}) => {
setPageIndex(pageIndex)
}}
/>
</Table.Container>
)
}
39 changes: 39 additions & 0 deletions src/DataTable/ErrorDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react'
import {ConfirmationDialog} from '../Dialog/ConfirmationDialog'

export type TableErrorDialogProps = React.PropsWithChildren<{
/**
* Provide an optional title for the dialog
* @default 'Error'
*/
title?: string

/**
* Provide an optional handler to be called when the user confirms to retry
*/
onRetry?: () => void

/**
* Provide an optional handler to be called when the user dismisses the dialog
*/
onDismiss?: () => void
}>

export function ErrorDialog({title = 'Error', children, onRetry, onDismiss}: TableErrorDialogProps) {
return (
<ConfirmationDialog
title={title}
onClose={gesture => {
if (gesture === 'confirm') {
onRetry?.()
} else {
onDismiss?.()
}
}}
confirmButtonContent="Retry"
cancelButtonContent="Dismiss"
>
{children}
</ConfirmationDialog>
)
}
5 changes: 3 additions & 2 deletions src/DataTable/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -501,12 +501,13 @@ export type TableTitleProps = React.PropsWithChildren<{
id: string
}>

function TableTitle({as = 'h2', children, id}: TableTitleProps) {
const TableTitle = React.forwardRef<HTMLElement, TableTitleProps>(function TableTitle({as = 'h2', children, id}, ref) {
return (
<Box
as={as}
className="TableTitle"
id={id}
ref={ref}
sx={{
color: 'fg.default',
fontWeight: 'bold',
Expand All @@ -518,7 +519,7 @@ function TableTitle({as = 'h2', children, id}: TableTitleProps) {
{children}
</Box>
)
}
})

export type TableSubtitleProps = React.PropsWithChildren<{
/**
Expand Down
73 changes: 73 additions & 0 deletions src/DataTable/__tests__/ErrorDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import userEvent from '@testing-library/user-event'
import {render, screen} from '@testing-library/react'
import React from 'react'
import {ErrorDialog} from '../ErrorDialog'

describe('Table.ErrorDialog', () => {
it('should use a default title of "Error" if `title` is not provided', () => {
render(<ErrorDialog />)
expect(
screen.getByRole('alertdialog', {
name: 'Error',
}),
).toBeInTheDocument()
})

it('should allow customizing the title of the dialog through `title`', () => {
const customTitle = 'custom-title'
render(<ErrorDialog title={customTitle} />)
expect(
screen.getByRole('alertdialog', {
name: customTitle,
}),
).toBeInTheDocument()
})

it('should use "Retry" as the confirm text of the dialog', () => {
render(<ErrorDialog />)
expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument()
})

it('should call `onRetry` if the confirm button is interacted with', async () => {
const user = userEvent.setup()
const onRetry = jest.fn()

render(<ErrorDialog onRetry={onRetry} />)
await user.click(screen.getByText('Retry'))
expect(onRetry).toHaveBeenCalledTimes(1)

onRetry.mockClear()

await user.keyboard('{Enter}')
expect(onRetry).toHaveBeenCalledTimes(1)
})

it('should set "Dismiss" as the cancel text of the dialog', () => {
render(<ErrorDialog />)
expect(screen.getByRole('button', {name: 'Dismiss'})).toBeInTheDocument()
})

it('should call `onDismiss` if the cancel button is interacted with', async () => {
const user = userEvent.setup()
const onDismiss = jest.fn()

render(<ErrorDialog onDismiss={onDismiss} />)
await user.click(screen.getByText('Dismiss'))
expect(onDismiss).toHaveBeenCalledTimes(1)

onDismiss.mockClear()

await user.keyboard('{Enter}')
expect(onDismiss).toHaveBeenCalledTimes(1)
})

it('should render `children` as the content of the dialog', () => {
render(
<ErrorDialog>
<span data-testid="children">children</span>
</ErrorDialog>,
)

expect(screen.getByRole('alertdialog', {name: 'Error'})).toContainElement(screen.getByTestId('children'))
})
})
2 changes: 2 additions & 0 deletions src/DataTable/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {DataTable} from './DataTable'
import {ErrorDialog} from './ErrorDialog'
import {
Table as TableImpl,
TableHead,
Expand Down Expand Up @@ -30,6 +31,7 @@ const Table = Object.assign(TableImpl, {
Cell: TableCell,
CellPlaceholder: TableCellPlaceholder,
Pagination,
ErrorDialog,
})

export {DataTable, Table}
Expand Down
Loading

0 comments on commit 8abf268

Please sign in to comment.