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 {Outlet} from "react-router-dom";
|
||||||
import {ToggleSwitch} from "../src/Components/Forms/ToggleSwitch";
|
import {ToggleSwitch} from "../src/Components/Forms/ToggleSwitch";
|
||||||
import {Step, Steps} from "../src/Components/Steps/Steps";
|
import {Step, Steps} from "../src/Components/Steps/Steps";
|
||||||
|
import {AsyncPaginate, AutoPaginate, Paginate} from "../src/Components/Pagination/Paginate";
|
||||||
|
|
||||||
export function DemoApp()
|
export function DemoApp()
|
||||||
{
|
{
|
||||||
|
@ -31,6 +32,8 @@ export function DemoApp()
|
||||||
|
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
|
|
||||||
|
const [page, setPage] = useState(11);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Application>
|
<Application>
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
|
@ -63,9 +66,7 @@ export function DemoApp()
|
||||||
<h2>TODO</h2>
|
<h2>TODO</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Pagination</li>
|
<li>Errors</li>
|
||||||
<li>Global states</li>
|
|
||||||
<li>Async</li>
|
|
||||||
<li>Subapps</li>
|
<li>Subapps</li>
|
||||||
<li>Modals</li>
|
<li>Modals</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -283,7 +284,7 @@ export function DemoApp()
|
||||||
<h3>Simple loaders</h3>
|
<h3>Simple loaders</h3>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<SpinningLoader/>
|
<SpinningLoader inline={true} />
|
||||||
|
|
||||||
<ListLoader/>
|
<ListLoader/>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -410,6 +411,37 @@ export function DemoApp()
|
||||||
</Card>
|
</Card>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</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>
|
</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/_list";
|
||||||
@import "components/_loaders";
|
@import "components/_loaders";
|
||||||
@import "components/_menus";
|
@import "components/_menus";
|
||||||
|
@import "components/_pagination";
|
||||||
@import "components/_select";
|
@import "components/_select";
|
||||||
@import "components/_steps";
|
@import "components/_steps";
|
||||||
@import "components/_steps-counter";
|
@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