Skip to content

Commit

Permalink
Make pagination more compact (#4882)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
WithoutPants authored Jun 11, 2024
1 parent e843c89 commit 621e890
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 61 deletions.
210 changes: 150 additions & 60 deletions ui/v2.5/src/components/List/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,132 @@
import React from "react";
import { Button, ButtonGroup } from "react-bootstrap";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
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 {
itemsPerPage: number;
Expand All @@ -23,91 +149,55 @@ export const Pagination: React.FC<IPaginationProps> = ({
totalItems,
onChangePage,
}) => {
const totalPages = Math.ceil(totalItems / itemsPerPage);

let startPage: number;
let endPage: number;
if (totalPages <= 10) {
// 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 intl = useIntl();

const pages = [...Array(endPage + 1 - startPage).keys()].map(
(i) => startPage + i
const totalPages = useMemo(
() => Math.ceil(totalItems / itemsPerPage),
[totalItems, itemsPerPage]
);

const calculatePageClass = (buttonPage: number) => {
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 />;
if (totalPages <= 1) return <div />;

return (
<ButtonGroup className="filter-container pagination">
<ButtonGroup className="pagination">
<Button
variant="secondary"
disabled={currentPage === 1}
onClick={() => onChangePage(1)}
title={intl.formatMessage({ id: "pagination.first" })}
>
<span className="d-none d-sm-inline">
<FormattedMessage id="pagination.first" />
</span>
<span className="d-inline d-sm-none">&#x300a;</span>
<span>«</span>
</Button>
<Button
className="d-none d-sm-block"
variant="secondary"
disabled={currentPage === 1}
onClick={() => onChangePage(currentPage - 1)}
title={intl.formatMessage({ id: "pagination.previous" })}
>
<FormattedMessage id="pagination.previous" />
&lt;
</Button>
{pageButtons}

<PageCount
totalPages={totalPages}
currentPage={currentPage}
onChangePage={onChangePage}
/>

<Button
className="d-none d-sm-block"
variant="secondary"
disabled={currentPage === totalPages}
onClick={() => onChangePage(currentPage + 1)}
title={intl.formatMessage({ id: "pagination.next" })}
>
<FormattedMessage id="pagination.next" />
&gt;
</Button>
<Button
variant="secondary"
disabled={currentPage === totalPages}
onClick={() => onChangePage(totalPages)}
title={intl.formatMessage({ id: "pagination.last" })}
>
<span className="d-none d-sm-inline">
<FormattedMessage id="pagination.last" />
</span>
<span className="d-inline d-sm-none">&#x300b;</span>
<span>»</span>
</Button>
</ButtonGroup>
);
Expand Down
13 changes: 13 additions & 0 deletions ui/v2.5/src/components/List/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@
padding-right: 15px;
transition: none;

&.page-count {
padding-right: 5px;
}

&.page-count-dropdown {
padding-left: 5px;
}

&:first-child {
border-left: none;
border-right: none;
}

&:last-child {
border-right: none;
}
}

.page-count-container .btn {
border-radius: 0;
}
}

.center-text {
Expand Down
8 changes: 7 additions & 1 deletion ui/v2.5/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,8 @@ div.dropdown-menu {
}

.filter-container,
.operation-container {
.operation-container,
.pagination {
align-items: center;
display: flex;
justify-content: center;
Expand Down Expand Up @@ -1330,6 +1331,11 @@ $detailTabWidth: calc(100% / 3);
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 {
display: grid;
grid-column-gap: 10px;
Expand Down
1 change: 1 addition & 0 deletions ui/v2.5/src/locales/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,7 @@
"version": "Version"
},
"pagination": {
"current_total": "{current} of {total}",
"first": "First",
"last": "Last",
"next": "Next",
Expand Down

0 comments on commit 621e890

Please sign in to comment.