Make pagination more compact (#4882)

* Make pagination more compact

Support entering page number or clicking from drop down

* Fix border radius in dropdown in btn group
* Separate page count control
This commit is contained in:
WithoutPants
2024-06-11 11:35:28 +10:00
committed by GitHub
parent e843c890fb
commit 621e890a48
4 changed files with 171 additions and 61 deletions

View File

@@ -1,6 +1,132 @@
import React from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { Button, ButtonGroup } from "react-bootstrap"; import {
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; Button,
ButtonGroup,
Dropdown,
Form,
InputGroup,
Overlay,
Popover,
} from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import useFocus from "src/utils/focus";
import { Icon } from "../Shared/Icon";
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
const PageCount: React.FC<{
totalPages: number;
currentPage: number;
onChangePage: (page: number) => void;
}> = ({ totalPages, currentPage, onChangePage }) => {
const intl = useIntl();
const currentPageCtrl = useRef(null);
const [pageInput, pageFocus] = useFocus();
const [showSelectPage, setShowSelectPage] = useState(false);
useEffect(() => {
if (showSelectPage) {
pageFocus();
}
}, [showSelectPage, pageFocus]);
const pageOptions = useMemo(() => {
const maxPagesToShow = 10;
const min = Math.max(1, currentPage - maxPagesToShow / 2);
const max = Math.min(min + maxPagesToShow, totalPages);
const pages = [];
for (let i = min; i <= max; i++) {
pages.push(i);
}
return pages;
}, [totalPages, currentPage]);
function onCustomChangePage() {
const newPage = Number.parseInt(pageInput.current?.value ?? "0");
if (newPage) {
onChangePage(newPage);
}
setShowSelectPage(false);
}
return (
<div className="page-count-container">
<ButtonGroup>
<Button
variant="secondary"
className="page-count"
ref={currentPageCtrl}
onClick={() => {
setShowSelectPage(true);
pageFocus();
}}
>
<FormattedMessage
id="pagination.current_total"
values={{
current: intl.formatNumber(currentPage),
total: intl.formatNumber(totalPages),
}}
/>
</Button>
<Dropdown>
<Dropdown.Toggle variant="secondary" className="page-count-dropdown">
<Icon size="xs" icon={faChevronDown} />
</Dropdown.Toggle>
<Dropdown.Menu>
{pageOptions.map((s) => (
<Dropdown.Item
key={s}
active={s === currentPage}
onClick={() => onChangePage(s)}
>
{s}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
</ButtonGroup>
<Overlay
target={currentPageCtrl.current}
show={showSelectPage}
placement="bottom"
rootClose
onHide={() => setShowSelectPage(false)}
>
<Popover id="select_page_popover">
<Form inline>
<InputGroup>
<Form.Control
type="number"
min={1}
max={totalPages}
className="text-input"
ref={pageInput}
defaultValue={currentPage}
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onCustomChangePage();
e.preventDefault();
}
}}
onFocus={(e: React.FocusEvent<HTMLInputElement>) =>
e.target.select()
}
/>
<InputGroup.Append>
<Button variant="primary" onClick={() => onCustomChangePage()}>
<Icon icon={faCheck} />
</Button>
</InputGroup.Append>
</InputGroup>
</Form>
</Popover>
</Overlay>
</div>
);
};
interface IPaginationProps { interface IPaginationProps {
itemsPerPage: number; itemsPerPage: number;
@@ -23,91 +149,55 @@ export const Pagination: React.FC<IPaginationProps> = ({
totalItems, totalItems,
onChangePage, onChangePage,
}) => { }) => {
const totalPages = Math.ceil(totalItems / itemsPerPage); const intl = useIntl();
let startPage: number; const totalPages = useMemo(
let endPage: number; () => Math.ceil(totalItems / itemsPerPage),
if (totalPages <= 10) { [totalItems, itemsPerPage]
// less than 10 total pages so show all
startPage = 1;
endPage = totalPages;
} else if (currentPage <= 6) {
startPage = 1;
endPage = 10;
} else if (currentPage + 4 >= totalPages) {
startPage = totalPages - 9;
endPage = totalPages;
} else {
startPage = currentPage - 5;
endPage = currentPage + 4;
}
const pages = [...Array(endPage + 1 - startPage).keys()].map(
(i) => startPage + i
); );
const calculatePageClass = (buttonPage: number) => { if (totalPages <= 1) return <div />;
if (pages.length <= 4) return "";
if (currentPage === 1 && buttonPage <= 4) return "";
const maxPage = pages[pages.length - 1];
if (currentPage === maxPage && buttonPage > maxPage - 3) return "";
if (Math.abs(buttonPage - currentPage) <= 1) return "";
return "d-none d-sm-block";
};
const pageButtons = pages.map((page: number) => (
<Button
variant="secondary"
className={calculatePageClass(page)}
key={page}
active={currentPage === page}
onClick={() => onChangePage(page)}
>
<FormattedNumber value={page} />
</Button>
));
if (pages.length <= 1) return <div />;
return ( return (
<ButtonGroup className="filter-container pagination"> <ButtonGroup className="pagination">
<Button <Button
variant="secondary" variant="secondary"
disabled={currentPage === 1} disabled={currentPage === 1}
onClick={() => onChangePage(1)} onClick={() => onChangePage(1)}
title={intl.formatMessage({ id: "pagination.first" })}
> >
<span className="d-none d-sm-inline"> <span>«</span>
<FormattedMessage id="pagination.first" />
</span>
<span className="d-inline d-sm-none">&#x300a;</span>
</Button> </Button>
<Button <Button
className="d-none d-sm-block"
variant="secondary" variant="secondary"
disabled={currentPage === 1} disabled={currentPage === 1}
onClick={() => onChangePage(currentPage - 1)} onClick={() => onChangePage(currentPage - 1)}
title={intl.formatMessage({ id: "pagination.previous" })}
> >
<FormattedMessage id="pagination.previous" /> &lt;
</Button> </Button>
{pageButtons}
<PageCount
totalPages={totalPages}
currentPage={currentPage}
onChangePage={onChangePage}
/>
<Button <Button
className="d-none d-sm-block"
variant="secondary" variant="secondary"
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
onClick={() => onChangePage(currentPage + 1)} onClick={() => onChangePage(currentPage + 1)}
title={intl.formatMessage({ id: "pagination.next" })}
> >
<FormattedMessage id="pagination.next" /> &gt;
</Button> </Button>
<Button <Button
variant="secondary" variant="secondary"
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
onClick={() => onChangePage(totalPages)} onClick={() => onChangePage(totalPages)}
title={intl.formatMessage({ id: "pagination.last" })}
> >
<span className="d-none d-sm-inline"> <span>»</span>
<FormattedMessage id="pagination.last" />
</span>
<span className="d-inline d-sm-none">&#x300b;</span>
</Button> </Button>
</ButtonGroup> </ButtonGroup>
); );

View File

@@ -7,14 +7,27 @@
padding-right: 15px; padding-right: 15px;
transition: none; transition: none;
&.page-count {
padding-right: 5px;
}
&.page-count-dropdown {
padding-left: 5px;
}
&:first-child { &:first-child {
border-left: none; border-left: none;
border-right: none;
} }
&:last-child { &:last-child {
border-right: none; border-right: none;
} }
} }
.page-count-container .btn {
border-radius: 0;
}
} }
.center-text { .center-text {

View File

@@ -713,7 +713,8 @@ div.dropdown-menu {
} }
.filter-container, .filter-container,
.operation-container { .operation-container,
.pagination {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -1330,6 +1331,11 @@ $detailTabWidth: calc(100% / 3);
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.btn-group > .dropdown:not(:first-child) > .btn {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
dl.details-list { dl.details-list {
display: grid; display: grid;
grid-column-gap: 10px; grid-column-gap: 10px;

View File

@@ -1160,6 +1160,7 @@
"version": "Version" "version": "Version"
}, },
"pagination": { "pagination": {
"current_total": "{current} of {total}",
"first": "First", "first": "First",
"last": "Last", "last": "Last",
"next": "Next", "next": "Next",