Add pagination components.
This commit is contained in:
parent
9fadcfb1b2
commit
7bde352ea7
6 changed files with 384 additions and 5 deletions
|
@ -24,6 +24,7 @@ import {Application} from "../src/Application/Application";
|
|||
import {Outlet} from "react-router-dom";
|
||||
import {ToggleSwitch} from "../src/Components/Forms/ToggleSwitch";
|
||||
import {Step, Steps} from "../src/Components/Steps/Steps";
|
||||
import {AsyncPaginate, AutoPaginate, Paginate} from "../src/Components/Pagination/Paginate";
|
||||
|
||||
export function DemoApp()
|
||||
{
|
||||
|
@ -31,6 +32,8 @@ export function DemoApp()
|
|||
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const [page, setPage] = useState(11);
|
||||
|
||||
return (
|
||||
<Application>
|
||||
<MainMenu>
|
||||
|
@ -63,9 +66,7 @@ export function DemoApp()
|
|||
<h2>TODO</h2>
|
||||
|
||||
<ul>
|
||||
<li>Pagination</li>
|
||||
<li>Global states</li>
|
||||
<li>Async</li>
|
||||
<li>Errors</li>
|
||||
<li>Subapps</li>
|
||||
<li>Modals</li>
|
||||
</ul>
|
||||
|
@ -283,7 +284,7 @@ export function DemoApp()
|
|||
<h3>Simple loaders</h3>
|
||||
|
||||
<Card>
|
||||
<SpinningLoader/>
|
||||
<SpinningLoader inline={true} />
|
||||
|
||||
<ListLoader/>
|
||||
</Card>
|
||||
|
@ -410,6 +411,37 @@ export function DemoApp()
|
|||
</Card>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<h2>Pagination</h2>
|
||||
|
||||
<h3>Normal pagination</h3>
|
||||
|
||||
<Paginate onChange={setPage} count={72} page={page}>
|
||||
<Card>Page {page}</Card>
|
||||
</Paginate>
|
||||
|
||||
<h3>Auto pagination</h3>
|
||||
|
||||
<AutoPaginate count={55}>
|
||||
{(page) => (
|
||||
<Card>Page {page}</Card>
|
||||
)}
|
||||
</AutoPaginate>
|
||||
|
||||
<h3>Async pagination</h3>
|
||||
|
||||
<AsyncPaginate count={async () => { return 72; }} getData={async () => (["a", Math.random(), "c"])}>
|
||||
{(data) => (
|
||||
<>
|
||||
{
|
||||
data.map((value, index) => (
|
||||
<div key={index}>{value}</div>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</AsyncPaginate>
|
||||
|
||||
</Application>
|
||||
);
|
||||
}
|
||||
|
|
137
src/Components/Pagination/Paginate.tsx
Normal file
137
src/Components/Pagination/Paginate.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
import React, {useCallback, useState} from "react";
|
||||
import {Pagination} from "./Pagination";
|
||||
import {SpinningLoader} from "../Loaders/SpinningLoader";
|
||||
import {Await, useAsync} from "../../Async";
|
||||
|
||||
/**
|
||||
* Paginated content component with custom page handling.
|
||||
*/
|
||||
export function Paginate({ page, onChange, count, children }: React.PropsWithChildren<{
|
||||
/**
|
||||
* The current page.
|
||||
*/
|
||||
page: number;
|
||||
|
||||
/**
|
||||
* Called when a new page is selected.
|
||||
* @param newPage The newly selected page.
|
||||
*/
|
||||
onChange: (newPage: number) => void;
|
||||
|
||||
/**
|
||||
* Pages count.
|
||||
*/
|
||||
count: number;
|
||||
}>)
|
||||
{
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
|
||||
<Pagination page={page} onChange={onChange} count={count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated content component.
|
||||
*/
|
||||
export function AutoPaginate({ count, children }: {
|
||||
/**
|
||||
* Pages count.
|
||||
*/
|
||||
count: number;
|
||||
|
||||
/**
|
||||
* Show the given page.
|
||||
* @param page The page to show.
|
||||
*/
|
||||
children: (page: number) => React.ReactElement;
|
||||
})
|
||||
{
|
||||
// The current page.
|
||||
const [page, setPage] = useState<number>(1);
|
||||
|
||||
return (
|
||||
<Paginate page={page} onChange={setPage} count={count}>
|
||||
{children(page)}
|
||||
</Paginate>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous paginated content component.
|
||||
*/
|
||||
export function AsyncPaginate<T>({ count, getData, children }: {
|
||||
/**
|
||||
* Get pages count.
|
||||
*/
|
||||
count: () => Promise<number>;
|
||||
|
||||
/**
|
||||
* Get data for the given page.
|
||||
* @param page The page for which to get data.
|
||||
*/
|
||||
getData: (page: number) => Promise<T>;
|
||||
|
||||
/**
|
||||
* Show the current page with its retrieved data.
|
||||
* @param data Data of the page to show.
|
||||
* @param page The page to show.
|
||||
*/
|
||||
children: (data: T, page: number) => React.ReactElement;
|
||||
})
|
||||
{
|
||||
// Getting pages count.
|
||||
const asyncCount = useAsync(count, []);
|
||||
|
||||
return (
|
||||
<Await async={asyncCount} fallback={<SpinningLoader />}>
|
||||
{
|
||||
(count) => (
|
||||
<AutoPaginate count={count}>
|
||||
{(page) => <AsyncPage page={page} getData={getData} render={children} />}
|
||||
</AutoPaginate>
|
||||
)
|
||||
}
|
||||
</Await>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* An async page to render.
|
||||
*/
|
||||
export function AsyncPage<T>({page, getData, render}: {
|
||||
/**
|
||||
* The page number to show.
|
||||
*/
|
||||
page: number;
|
||||
|
||||
/**
|
||||
* Get data for the given page.
|
||||
* @param page The page for which to get data.
|
||||
*/
|
||||
getData: (page: number) => Promise<T>;
|
||||
|
||||
/**
|
||||
* Render the page with its retrieved data.
|
||||
* @param data Data of the page to show.
|
||||
* @param page The page to show.
|
||||
*/
|
||||
render: (data: T, page: number) => React.ReactElement;
|
||||
})
|
||||
{
|
||||
// Store function to get page data.
|
||||
const getPageData = useCallback(() => {
|
||||
return getData(page);
|
||||
}, [page]);
|
||||
|
||||
// Getting page data.
|
||||
const asyncPageData = useAsync(getPageData, [getPageData]);
|
||||
|
||||
return (
|
||||
<Await async={asyncPageData} fallback={<SpinningLoader />}>
|
||||
{(pageData) => render(pageData, page)}
|
||||
</Await>
|
||||
);
|
||||
}
|
141
src/Components/Pagination/Pagination.tsx
Normal file
141
src/Components/Pagination/Pagination.tsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import {CaretLeft, CaretRight} from "@phosphor-icons/react";
|
||||
import {Tooltip} from "../Floating/Tooltip";
|
||||
import {usePreviousValue} from "../../Utils";
|
||||
|
||||
/**
|
||||
* Pagination component.
|
||||
*/
|
||||
export function Pagination({ page, onChange, count }: {
|
||||
/**
|
||||
* The current page.
|
||||
*/
|
||||
page: number;
|
||||
|
||||
/**
|
||||
* Called when a new page is selected.
|
||||
* @param newPage The newly selected page.
|
||||
*/
|
||||
onChange: (newPage: number) => void;
|
||||
|
||||
/**
|
||||
* Pages count.
|
||||
*/
|
||||
count: number;
|
||||
})
|
||||
{
|
||||
// Memorize previous page.
|
||||
const previousPage = usePreviousValue(page);
|
||||
|
||||
// The input text to use, when the currently entered value is not a number.
|
||||
const [inputText, setInputText] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (page != previousPage)
|
||||
// If the page has changed, resetting the input text.
|
||||
setInputText(undefined);
|
||||
}, [previousPage, page]);
|
||||
|
||||
return (
|
||||
<nav className={"pages"}>
|
||||
<Tooltip content={"Previous"}>
|
||||
<button className={"flat icon-only"} disabled={page <= 1} onClick={() => onChange(page - 1)}>
|
||||
<CaretLeft/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<ul>
|
||||
{ // First page, only show when the current page isn't the first page.
|
||||
page > 1 &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={() => onChange(1)}>1</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
{ // Space between first page and shown pages.
|
||||
// Only show when the page isn't the first page.
|
||||
page - 4 > 1 &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={page - 4 > 2 ? undefined : () => onChange(page - 4)}>{page - 4 > 2 ? "···" : page - 4}</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
{ // Only show when the page isn't the first page.
|
||||
page - 3 > 1 &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={() => onChange(page - 3)}>{page - 3}</button>
|
||||
</li>
|
||||
}
|
||||
{ // Only show when the page isn't the first page.
|
||||
page - 2 > 1 &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={() => onChange(page - 2)}>{page - 2}</button>
|
||||
</li>
|
||||
}
|
||||
{ // Only show when the page isn't the first page.
|
||||
page - 1 > 1 &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={() => onChange(page - 1)}>{page - 1}</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
{ // Current page input.
|
||||
<li className={"current"}>
|
||||
<input type={"number"} step={1} min={1} max={count} value={inputText ?? page} onChange={(event) => {
|
||||
// Try to get the new value, between 1 and page count.
|
||||
const newValue = Math.max(Math.min(event.currentTarget.valueAsNumber, count), 1);
|
||||
if (isNaN(newValue))
|
||||
// Not a numeric value, keep the current input text without changing the page number.
|
||||
setInputText(event.currentTarget.value);
|
||||
else
|
||||
{ // The value has been read successfully, changing it and resetting input text.
|
||||
onChange(newValue);
|
||||
setInputText(undefined);
|
||||
}
|
||||
}} />
|
||||
</li>
|
||||
}
|
||||
|
||||
{ // Only show when the page isn't the last page.
|
||||
page + 1 < count &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={() => onChange(page + 1)}>{page + 1}</button>
|
||||
</li>
|
||||
}
|
||||
{ // Only show when the page isn't the last page.
|
||||
page + 2 < count &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={() => onChange(page + 2)}>{page + 2}</button>
|
||||
</li>
|
||||
}
|
||||
{ // Only show when the page isn't the last page.
|
||||
page + 3 < count &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={() => onChange(page + 3)}>{page + 3}</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
{ // Space between shown pages and last page.
|
||||
// Only show when the page isn't the last page.
|
||||
page + 4 < count &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={page + 4 < (count - 1) ? undefined : () => onChange(page + 4)}>{page + 4 < (count - 1) ? "···" : page + 4}</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
{ // Last page, only show when the current page isn't the last page.
|
||||
page < count &&
|
||||
<li>
|
||||
<button className={"flat"} onClick={() => onChange(count)}>{count}</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<Tooltip content={"Next"}>
|
||||
<button className={"flat icon-only"} disabled={page >= count} onClick={() => onChange(page + 1)}>
|
||||
<CaretRight/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</nav>
|
||||
);
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
@import "components/_list";
|
||||
@import "components/_loaders";
|
||||
@import "components/_menus";
|
||||
@import "components/_pagination";
|
||||
@import "components/_select";
|
||||
@import "components/_steps";
|
||||
@import "components/_steps-counter";
|
||||
|
|
1
src/styles/components/_pagination.less
Normal file
1
src/styles/components/_pagination.less
Normal file
|
@ -0,0 +1 @@
|
|||
@import "pagination/_nav";
|
67
src/styles/components/pagination/_nav.less
Normal file
67
src/styles/components/pagination/_nav.less
Normal file
|
@ -0,0 +1,67 @@
|
|||
nav.pages
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin: auto;
|
||||
padding: 0.25em;
|
||||
width: 50em;
|
||||
max-width: 95%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0.25em;
|
||||
|
||||
border: solid var(--background-darkest) thin;
|
||||
background: var(--background-lighter);
|
||||
|
||||
button, .button
|
||||
{
|
||||
margin: auto;
|
||||
border: none;
|
||||
|
||||
&:hover
|
||||
{
|
||||
background: var(--background-darker);
|
||||
}
|
||||
}
|
||||
|
||||
input
|
||||
{
|
||||
transition: background 0.2s ease;
|
||||
width: 5em;
|
||||
border-color: var(--background-darker);
|
||||
outline: none;
|
||||
|
||||
text-align: center;
|
||||
|
||||
&:focus
|
||||
{
|
||||
background: var(--background-darker);
|
||||
}
|
||||
}
|
||||
|
||||
> ul
|
||||
{
|
||||
flex: 1;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
> li
|
||||
{
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
|
||||
button, .button
|
||||
{
|
||||
margin: auto 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue