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(DataTable): add Table.ErrorDialog component #3276

Merged
merged 7 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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