-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create StudioTable components (#12731)
- Loading branch information
1 parent
03e1633
commit c005b6c
Showing
18 changed files
with
1,093 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 72 additions & 0 deletions
72
...onents/src/components/StudioTableLocalPagination/StudioTableLocalPagination.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { Canvas, Meta } from '@storybook/blocks'; | ||
import { Heading, Paragraph } from '@digdir/design-system-react'; | ||
import * as StudioTableLocalPaginationStories from './StudioTableLocalPagination.stories'; | ||
import { StudioTableLocalPagination } from './StudioTableLocalPagination'; | ||
|
||
<Meta of={StudioTableLocalPaginationStories} /> | ||
|
||
<Heading level={1} size='small'> | ||
StudioTableLocalPagination | ||
</Heading> | ||
<Paragraph> | ||
The StudioTableLocalPagination component handles pagination internally, eliminating the need for | ||
manual control. It seamlessly manages pagination logic for you. | ||
</Paragraph> | ||
|
||
<Canvas of={StudioTableLocalPaginationStories.Preview} /> | ||
|
||
<Heading level={2} size='xsmall'> | ||
Column format | ||
</Heading> | ||
<Paragraph>Columns must have both an `accessor` and a `value` property.</Paragraph> | ||
|
||
```tsx | ||
const columns = [ | ||
{ | ||
accessor: 'icon', | ||
value: '', | ||
}, | ||
{ | ||
accessor: 'name', | ||
value: 'Name', | ||
}, | ||
{ | ||
accessor: 'creator', | ||
value: 'Created by', | ||
}, | ||
{ | ||
accessor: 'lastChanged', | ||
value: 'Last changed', | ||
}, | ||
]; | ||
``` | ||
|
||
<Heading level={2} size='xsmall'> | ||
Row format | ||
</Heading> | ||
<ul> | ||
<li>Rows must have an `id` that is unique.</li> | ||
<li> | ||
The accessors in the columns array is used to display the row properties. Therefore, each | ||
property name has to exactly match the accessor. | ||
</li> | ||
</ul> | ||
|
||
```tsx | ||
const rows = [ | ||
{ | ||
id: 1, | ||
icon: <StarFillIcon />, | ||
name: 'Coordinated register notification', | ||
creator: 'Brønnøysund Register Centre', | ||
lastChanged: '12-04-2023', | ||
}, | ||
{ | ||
id: 2, | ||
icon: <StarFillIcon />, | ||
name: 'Application for authorisation and license as a healthcare personnel', | ||
creator: 'The Norwegian Directorate of Health', | ||
lastChanged: '05-04-2023', | ||
}, | ||
]; | ||
``` |
54 changes: 54 additions & 0 deletions
54
...mponents/src/components/StudioTableLocalPagination/StudioTableLocalPagination.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import React from 'react'; | ||
import type { Meta, StoryFn } from '@storybook/react'; | ||
import { StudioTableLocalPagination } from './StudioTableLocalPagination'; | ||
import { columns, rows } from '../StudioTableRemotePagination/mockData'; | ||
|
||
type Story = StoryFn<typeof StudioTableLocalPagination>; | ||
|
||
const meta: Meta = { | ||
title: 'Studio/StudioTableLocalPagination', | ||
component: StudioTableLocalPagination, | ||
argTypes: { | ||
columns: { | ||
description: 'An array of objects representing the table columns.', | ||
}, | ||
rows: { | ||
description: 'An array of objects representing the table rows.', | ||
}, | ||
size: { | ||
control: 'radio', | ||
options: ['small', 'medium', 'large'], | ||
description: 'The size of the table.', | ||
}, | ||
emptyTableMessage: { | ||
description: 'The message to display when the table is empty.', | ||
}, | ||
isSortable: { | ||
description: | ||
'Boolean that sets sorting to true or false. If set to false, the sorting buttons are hidden.', | ||
}, | ||
pagination: { | ||
description: | ||
'An object containing pagination-related props. If not provided, pagination is hidden.', | ||
}, | ||
}, | ||
}; | ||
|
||
export const Preview: Story = (args) => ( | ||
<StudioTableLocalPagination | ||
columns={columns} | ||
rows={rows} | ||
size={args.size} | ||
emptyTableMessage={'No data found'} | ||
isSortable={true} | ||
pagination={{ | ||
pageSizeOptions: [5, 10, 20, 50], | ||
pageSizeLabel: 'Rows per page', | ||
nextButtonText: 'Next', | ||
previousButtonText: 'Previous', | ||
itemLabel: (num) => `Page ${num}`, | ||
}} | ||
/> | ||
); | ||
|
||
export default meta; |
145 changes: 145 additions & 0 deletions
145
...-components/src/components/StudioTableLocalPagination/StudioTableLocalPagination.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import React from 'react'; | ||
import { render, screen, within } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { StudioTableLocalPagination } from './StudioTableLocalPagination'; | ||
import type { StudioTableLocalPaginationProps } from './StudioTableLocalPagination'; | ||
import { columns, rows } from '../StudioTableRemotePagination/mockData'; | ||
|
||
describe('StudioTableLocalPagination', () => { | ||
const paginationProps: StudioTableLocalPaginationProps['pagination'] = { | ||
pageSizeOptions: [5, 10, 50], | ||
pageSizeLabel: 'Rows per page', | ||
nextButtonText: 'Next', | ||
previousButtonText: 'Previous', | ||
itemLabel: (num) => `Page ${num}`, | ||
}; | ||
|
||
it('renders the table with columns and rows', () => { | ||
render(<StudioTableLocalPagination columns={columns} rows={rows} />); | ||
|
||
expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument(); | ||
expect(screen.getByRole('columnheader', { name: 'Created by' })).toBeInTheDocument(); | ||
expect( | ||
screen.getByRole('cell', { name: 'Coordinated register notification' }), | ||
).toBeInTheDocument(); | ||
expect( | ||
screen.getByRole('cell', { name: 'The Norwegian Directorate of Health' }), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it('does not render sorting buttons when isSortable is set to false', () => { | ||
render(<StudioTableLocalPagination columns={columns} rows={rows} isSortable={false} />); | ||
|
||
expect(screen.queryByRole('button', { name: 'Name' })).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('triggers sorting when a sortable column header is clicked', async () => { | ||
render(<StudioTableLocalPagination columns={columns} rows={rows} isSortable />); | ||
const user = userEvent.setup(); | ||
|
||
await user.click(screen.getByRole('button', { name: 'Name' })); | ||
const [, firstBodyRow, secondBodyRow] = screen.getAllByRole('row'); | ||
|
||
expect( | ||
within(firstBodyRow).getByRole('cell', { name: 'A-melding – all forms' }), | ||
).toBeInTheDocument(); | ||
|
||
expect( | ||
within(secondBodyRow).getByRole('cell', { name: 'Application for VAT registration' }), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the complete table when pagination prop is not provided', () => { | ||
render(<StudioTableLocalPagination columns={columns} rows={rows} />); | ||
expect( | ||
screen.getByRole('cell', { name: 'Coordinated register notification' }), | ||
).toBeInTheDocument(); | ||
expect( | ||
screen.getByRole('cell', { name: 'Application for a certificate of good conduct' }), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders pagination controls when pagination prop is provided', () => { | ||
render( | ||
<StudioTableLocalPagination columns={columns} rows={rows} pagination={paginationProps} />, | ||
); | ||
|
||
expect(screen.getByRole('combobox', { name: 'Rows per page' })).toBeInTheDocument(); | ||
expect(screen.getByRole('button', { name: 'Next' })).toBeInTheDocument(); | ||
}); | ||
|
||
it('changes page when the "Next" button is clicked', async () => { | ||
render( | ||
<StudioTableLocalPagination columns={columns} rows={rows} pagination={paginationProps} />, | ||
); | ||
const user = userEvent.setup(); | ||
|
||
await user.click(screen.getByRole('button', { name: 'Next' })); | ||
|
||
expect( | ||
screen.queryByRole('cell', { name: 'Coordinated register notification' }), | ||
).not.toBeInTheDocument(); | ||
expect(screen.getByRole('cell', { name: 'A-melding – all forms' })).toBeInTheDocument(); | ||
expect( | ||
screen.getByRole('cell', { name: 'Application for VAT registration' }), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it('changes page when the "Page 2" button is clicked', async () => { | ||
render( | ||
<StudioTableLocalPagination columns={columns} rows={rows} pagination={paginationProps} />, | ||
); | ||
const user = userEvent.setup(); | ||
|
||
await user.click(screen.getByRole('button', { name: 'Page 2' })); | ||
|
||
expect( | ||
screen.queryByRole('cell', { name: 'Coordinated register notification' }), | ||
).not.toBeInTheDocument(); | ||
expect(screen.getByRole('cell', { name: 'A-melding – all forms' })).toBeInTheDocument(); | ||
expect( | ||
screen.getByRole('cell', { name: 'Application for VAT registration' }), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it('changes page size when a different page size option is selected', async () => { | ||
render( | ||
<StudioTableLocalPagination columns={columns} rows={rows} pagination={paginationProps} />, | ||
); | ||
const user = userEvent.setup(); | ||
|
||
await user.selectOptions(screen.getByRole('combobox', { name: 'Rows per page' }), '10'); | ||
|
||
const tableBody = screen.getAllByRole('rowgroup')[1]; | ||
const tableBodyRows = within(tableBody).getAllByRole('row'); | ||
expect(tableBodyRows).toHaveLength(10); | ||
}); | ||
|
||
it('sets currentPage to 1 when no rows are displayed', async () => { | ||
render( | ||
<StudioTableLocalPagination columns={columns} rows={rows} pagination={paginationProps} />, | ||
); | ||
const user = userEvent.setup(); | ||
|
||
await user.click(screen.getByRole('button', { name: 'Page 4' })); | ||
const lastPageBody = screen.getAllByRole('rowgroup')[1]; | ||
const lastPageRow = within(lastPageBody).getAllByRole('row'); | ||
expect(lastPageRow.length).toBe(1); | ||
|
||
await user.selectOptions(screen.getByRole('combobox', { name: 'Rows per page' }), '50'); | ||
const tableBody = screen.getAllByRole('rowgroup')[1]; | ||
const tableBodyRows = within(tableBody).getAllByRole('row'); | ||
expect(tableBodyRows.length).toBeGreaterThan(10); | ||
}); | ||
|
||
it('displays the empty table message when there are no rows to display', () => { | ||
render( | ||
<StudioTableLocalPagination | ||
columns={columns} | ||
rows={[]} | ||
emptyTableMessage='No rows to display' | ||
/>, | ||
); | ||
expect(screen.getByText('No rows to display')).toBeInTheDocument(); | ||
}); | ||
}); |
75 changes: 75 additions & 0 deletions
75
...tudio-components/src/components/StudioTableLocalPagination/StudioTableLocalPagination.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import React, { forwardRef, useEffect, useState } from 'react'; | ||
import { StudioTableRemotePagination } from '../StudioTableRemotePagination'; | ||
import type { Rows } from '../StudioTableRemotePagination'; | ||
import { useTableSorting } from '../../hooks/useTableSorting'; | ||
import { getRowsToRender } from '../StudioTableRemotePagination/utils'; | ||
|
||
export type StudioTableLocalPaginationProps = { | ||
columns: Record<'accessor' | 'value', string>[]; | ||
rows: Rows; | ||
size?: 'small' | 'medium' | 'large'; | ||
emptyTableMessage?: string; | ||
isSortable?: boolean; | ||
pagination?: { | ||
pageSizeOptions: number[]; | ||
pageSizeLabel: string; | ||
nextButtonText: string; | ||
previousButtonText: string; | ||
itemLabel: (num: number) => string; | ||
}; | ||
}; | ||
|
||
export const StudioTableLocalPagination = forwardRef< | ||
HTMLTableElement, | ||
StudioTableLocalPaginationProps | ||
>( | ||
( | ||
{ columns, rows, isSortable = true, size = 'medium', emptyTableMessage, pagination }, | ||
ref, | ||
): React.ReactElement => { | ||
const [currentPage, setCurrentPage] = useState<number>(1); | ||
const [pageSize, setPageSize] = useState<number>(pagination?.pageSizeOptions[0] ?? undefined); | ||
|
||
const { handleSorting, sortedRows } = useTableSorting(rows, { enable: isSortable }); | ||
|
||
const initialRowsToRender = getRowsToRender(currentPage, pageSize, rows); | ||
const [rowsToRender, setRowsToRender] = useState<Rows>(initialRowsToRender); | ||
|
||
useEffect(() => { | ||
const newRowsToRender = getRowsToRender(currentPage, pageSize, sortedRows || rows); | ||
|
||
const isOutOfRange = !newRowsToRender.length && currentPage > 1; | ||
if (isOutOfRange) { | ||
setCurrentPage(1); | ||
setRowsToRender(getRowsToRender(1, pageSize, sortedRows || rows)); | ||
return; | ||
} | ||
|
||
setRowsToRender(newRowsToRender); | ||
}, [sortedRows, rows, currentPage, pageSize]); | ||
|
||
const totalPages = Math.ceil(rows.length / pageSize); | ||
|
||
const studioTableRemotePaginationProps = pagination && { | ||
...pagination, | ||
currentPage, | ||
totalPages, | ||
onPageChange: setCurrentPage, | ||
onPageSizeChange: setPageSize, | ||
}; | ||
|
||
return ( | ||
<StudioTableRemotePagination | ||
columns={columns} | ||
rows={rowsToRender} | ||
size={size} | ||
emptyTableMessage={emptyTableMessage} | ||
onSortClick={isSortable && handleSorting} | ||
pagination={studioTableRemotePaginationProps} | ||
ref={ref} | ||
/> | ||
); | ||
}, | ||
); | ||
|
||
StudioTableLocalPagination.displayName = 'StudioTableLocalPagination'; |
1 change: 1 addition & 0 deletions
1
frontend/libs/studio-components/src/components/StudioTableLocalPagination/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { StudioTableLocalPagination } from './StudioTableLocalPagination'; |
Oops, something went wrong.