-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Table): add page size select (#232)
- Loading branch information
1 parent
9aeee09
commit 4ea6838
Showing
26 changed files
with
3,333 additions
and
6,158 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
101 changes: 101 additions & 0 deletions
101
packages/visualizations-react/stories/Table/PaginatedTemplates.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,101 @@ | ||
import React, { useState, useEffect } from 'react'; | ||
import type { DataFrame, Pagination } from '@opendatasoft/visualizations'; | ||
import { Table } from '../../src'; | ||
import data from './data'; | ||
import options from './options'; | ||
|
||
const fetchData = async ({ size, page }: { size: number; page: number }) => { | ||
const startIndex = (page - 1) * size; | ||
const endIndex = startIndex + size; | ||
await setTimeout(() => {}, 300); | ||
const dataFrame: DataFrame = data?.slice(startIndex, endIndex); | ||
return dataFrame; | ||
}; | ||
|
||
// eslint-disable-next-line import/prefer-default-export | ||
export const usePaginatedData = ({ | ||
current, | ||
recordsPerPage, | ||
}: { | ||
current: number; | ||
recordsPerPage: number; | ||
}) => { | ||
const [records, setRecords] = useState<DataFrame>(); | ||
const [pageSize, setPageSize] = useState(recordsPerPage); | ||
const [page, setPage] = useState(current); | ||
|
||
useEffect(() => { | ||
(async () => { | ||
const newRecords = await fetchData({ | ||
size: pageSize, | ||
page, | ||
}); | ||
setRecords(newRecords); | ||
})(); | ||
}, [recordsPerPage, page, pageSize, setRecords]); | ||
|
||
const paginatedData = { value: records, isLoading: false }; | ||
const totalRecords = data.values.length; | ||
|
||
return { | ||
records, | ||
setRecords, | ||
pageSize, | ||
setPageSize, | ||
page, | ||
setPage, | ||
totalRecords, | ||
paginatedData, | ||
}; | ||
}; | ||
|
||
export const PaginatedTemplate = (pagination: Pagination) => { | ||
const { current = 1, recordsPerPage = 5 } = pagination; | ||
const { paginatedData, page, pageSize, setPage } = usePaginatedData({ | ||
current, | ||
recordsPerPage, | ||
}); | ||
|
||
const stateFulOptions = { | ||
...options, | ||
pagination: { | ||
current: page, | ||
recordsPerPage: pageSize, | ||
totalRecords: data.length, | ||
onPageChange: setPage, | ||
}, | ||
}; | ||
|
||
return <Table data={paginatedData} options={stateFulOptions} />; | ||
}; | ||
|
||
export const PageSizeTemplate = (pagination: Pagination) => { | ||
const { current = 1, recordsPerPage = 5 } = pagination; | ||
const { paginatedData, page, pageSize, setPage, setPageSize } = usePaginatedData({ | ||
current, | ||
recordsPerPage, | ||
}); | ||
|
||
const stateFulOptions = { | ||
...options, | ||
pagination: { | ||
current: page, | ||
recordsPerPage: pageSize, | ||
totalRecords: data.length, | ||
onPageChange: setPage, // | ||
pageSizeSelect: { | ||
options: [ | ||
{ label: '2 / pages', value: 2 }, | ||
{ label: '5 / pages', value: 5 }, | ||
{ label: '10 / pages', value: 10 }, | ||
], | ||
onChange: (newSize: number) => { | ||
setPageSize(newSize); | ||
setPage(1); | ||
}, // stateful, defined in template | ||
}, | ||
}, | ||
}; | ||
|
||
return <Table data={paginatedData} options={stateFulOptions} />; | ||
}; |
112 changes: 28 additions & 84 deletions
112
packages/visualizations-react/stories/Table/Pagination.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 |
---|---|---|
@@ -1,108 +1,52 @@ | ||
import React, { useState } from 'react'; | ||
import React from 'react'; | ||
import type { ComponentMeta, ComponentStory } from '@storybook/react'; | ||
import type { TableProps } from '@opendatasoft/visualizations'; | ||
import { Table } from '../../src'; | ||
import value from './data'; | ||
import options from './options'; | ||
import './pagination.css'; | ||
import { PaginatedTemplate, PageSizeTemplate } from './PaginatedTemplates'; | ||
|
||
const meta: ComponentMeta<typeof Table> = { | ||
title: 'Table/Pagination', | ||
component: Table, | ||
}; | ||
export default meta; | ||
|
||
const data: TableProps['data'] = { | ||
value, | ||
loading: false, | ||
}; | ||
|
||
const sliceDataX = (numberPerPage: number) => (fullData: TableProps['data'], page: number) => { | ||
const startIndex = (page - 1) * numberPerPage; | ||
return fullData.value?.slice(startIndex, startIndex + numberPerPage); | ||
}; | ||
|
||
const makeTemplate = (numberPerPage: number) => { | ||
const sliceData = sliceDataX(numberPerPage); | ||
const PaginatedTemplate: ComponentStory<typeof Table> = args => { | ||
const { data: rawData, options: paginatedOptions } = args; | ||
const { pagination } = paginatedOptions; | ||
const { initial = 0 } = pagination || {}; | ||
const [records, setRecords] = useState(sliceData(rawData, initial || 1)); | ||
if (paginatedOptions.pagination && rawData.value) { | ||
paginatedOptions.pagination.onChangePage = async (page: number) => { | ||
await setTimeout(() => { | ||
setRecords(sliceData(rawData, page)); | ||
}, 300); | ||
}; | ||
} | ||
|
||
const paginatedData = { value: records, isLoading: false }; | ||
return <Table data={paginatedData} options={paginatedOptions} />; | ||
}; | ||
return PaginatedTemplate; | ||
}; | ||
|
||
const paginatedOptions = { | ||
...options, | ||
pagination: { | ||
initial: 2, | ||
recordsPerPage: 3, | ||
totalRecords: value.length, | ||
onChangePage: () => {}, // set in template | ||
}, | ||
}; | ||
const PaginatedTemplate = makeTemplate(3); | ||
export const Paginated = PaginatedTemplate.bind({}); | ||
const PaginatedTable: ComponentStory<typeof PaginatedTemplate> = args => ( | ||
<PaginatedTemplate {...args} /> | ||
); | ||
export const Paginated = PaginatedTable.bind({}); | ||
Paginated.args = { | ||
data, | ||
options: paginatedOptions, | ||
}; | ||
|
||
const longPagesOptions = { | ||
...options, | ||
pagination: { | ||
initial: 1, | ||
recordsPerPage: 10, | ||
totalRecords: value.length, | ||
onChangePage: () => {}, // set in template | ||
}, | ||
}; | ||
const LongPaginatedTemplate = makeTemplate(10); | ||
export const LongPages = LongPaginatedTemplate.bind({}); | ||
LongPages.args = { | ||
data, | ||
options: longPagesOptions, | ||
current: 2, | ||
recordsPerPage: 3, | ||
}; | ||
|
||
const shortPagesOptions = { | ||
...options, | ||
pagination: { | ||
initial: 1, | ||
recordsPerPage: 2, | ||
totalRecords: value.length, | ||
onChangePage: () => {}, // set in template | ||
}, | ||
export const Longpagination = PaginatedTable.bind({}); | ||
Longpagination.args = { | ||
current: 1, | ||
recordsPerPage: 10, | ||
}; | ||
const ShortPaginatedTemplate = makeTemplate(2); | ||
export const ShortPages = ShortPaginatedTemplate.bind({}); | ||
ShortPages.args = { | ||
data, | ||
options: shortPagesOptions, | ||
export const Shortpagination = PaginatedTable.bind({}); | ||
Shortpagination.args = { | ||
current: 1, | ||
recordsPerPage: 2, | ||
}; | ||
|
||
|
||
const StyledPaginated: ComponentStory<typeof Table> = args => ( | ||
const StyledPaginated: ComponentStory<typeof PaginatedTemplate> = args => ( | ||
<div className="custom-pagination"> | ||
<PaginatedTemplate {...args} /> | ||
</div> | ||
); | ||
|
||
export const CustomStyle = StyledPaginated.bind({}); | ||
CustomStyle.args = { | ||
data, | ||
options: { | ||
...paginatedOptions, | ||
unstyled: true, | ||
}, | ||
current: 2, | ||
recordsPerPage: 3, | ||
}; | ||
|
||
const PageSizeTable: ComponentStory<typeof PageSizeTemplate> = args => ( | ||
<PageSizeTemplate {...args} /> | ||
); | ||
export const PageSize = PageSizeTable.bind({}); | ||
PageSize.args = { | ||
current: 2, | ||
recordsPerPage: 5, | ||
}; |
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,50 @@ | ||
import React from 'react'; | ||
import { Table } from 'src'; | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import data from 'stories/Table/data'; | ||
import options from 'stories/Table/options'; | ||
import { usePaginatedData } from 'stories/Table/PaginatedTemplates'; | ||
|
||
/* This template will fail to catch a new page and returns previous data: { | ||
value, | ||
loading: false, | ||
}, page and pageSize | ||
simulating e.g. an API call fail. | ||
The select should stay on it's previous value, not the clicked one. | ||
*/ | ||
const PageSizeFail = () => { | ||
const { paginatedData, setPage, setPageSize } = usePaginatedData({ | ||
current: 2, | ||
recordsPerPage: 5, | ||
}); | ||
|
||
const stateFulOptions = { | ||
...options, | ||
pagination: { | ||
current: 2, | ||
recordsPerPage: 5, | ||
totalRecords: data.length, | ||
onPageChange: () => setPage(2), // | ||
pageSizeSelect: { | ||
options: [ | ||
{ label: '2 / pages', value: 2 }, | ||
{ label: '5 / pages', value: 5 }, | ||
{ label: '10 / pages', value: 10 }, | ||
], | ||
onChange: () => setPageSize(5), // stateful, defined in template | ||
}, | ||
}, | ||
}; | ||
return <Table data={paginatedData} options={stateFulOptions} />; | ||
}; | ||
|
||
test('Page size select stays on the correct component if page change fails', async () => { | ||
const user = userEvent.setup(); | ||
render(<PageSizeFail />); | ||
|
||
expect(screen.getByRole('combobox')).toHaveValue('5'); | ||
|
||
await user.selectOptions(screen.getByRole('combobox'), '10'); | ||
expect(screen.getByRole('combobox')).toHaveValue('5'); | ||
}); |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
33 changes: 33 additions & 0 deletions
33
packages/visualizations/src/components/Pagination/PageSize.svelte
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,33 @@ | ||
<script lang="ts"> | ||
import { toNumber } from 'lodash'; | ||
import type { PageSizeOption } from './types'; | ||
export let options: PageSizeOption[]; | ||
export let onChange: (size: number) => void; | ||
export let selected: number; | ||
let value: number; | ||
/* This ensure a "controlled" behavior | ||
e.g. if the onChange fails it will take the value of selected, not the one the user clicked. | ||
I tried $: value = selected and bind:value={selected}, but they don't override user selection | ||
*/ | ||
$: if (value !== selected) { | ||
value = selected; | ||
} | ||
</script> | ||
|
||
<select bind:value on:change={(e) => onChange(toNumber(e.currentTarget.value))}> | ||
{#each options as option} | ||
<option label={option.label} value={option.value} /> | ||
{/each} | ||
</select> | ||
|
||
<style> | ||
:global(.ods-dataviz--default) select { | ||
background-color: white; | ||
padding: var(--spacing-50); | ||
border: solid 1px var(--border-color); | ||
border-radius: var(--border-radius-2); | ||
} | ||
</style> |
Oops, something went wrong.
4ea6838
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coverage for this commit
Coverage Report
4ea6838
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coverage for this commit
Coverage Report