Skip to content

Commit ead8571

Browse files
authored
refactor(content-explorer): refactor pagination footer (#4007)
* refactor(content-explorer): refactor pagination footer * refactor(content-explorer): update prop and style * refactor(content-explorer): add withPagination section * refactor(content-explorer): update test description
1 parent db5d673 commit ead8571

16 files changed

+560
-14
lines changed

i18n/en-US.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,10 +592,16 @@ be.noActivityCommentPrompt = Comment and @mention people to notify them.
592592
be.open = Open
593593
# Next page button tooltip
594594
be.pagination.nextPage = Next Page
595+
# Next page button
596+
be.pagination.nextPageButton = Next
597+
# Pagination menu status with the range of entries shown
598+
be.pagination.pageEntryStatus = Showing {startEntryIndex} to {endEntryIndex} of {totalCount} entries
595599
# Pagination menu button with current page number out of total number of pages
596600
be.pagination.pageStatus = {pageNumber} of {pageCount}
597601
# Previous page button tooltip
598602
be.pagination.previousPage = Previous Page
603+
# Previous page button
604+
be.pagination.previousPageButton = Previous
599605
# Icon title for a Box item of type folder that is private and has no collaborators
600606
be.personalFolder = Personal Folder
601607
# Message to the user to enter into point annotation mode

src/elements/common/messages.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,11 +916,21 @@ const messages = defineMessages({
916916
description: 'Next page button tooltip',
917917
id: 'be.pagination.nextPage',
918918
},
919+
nextPageButton: {
920+
defaultMessage: 'Next',
921+
description: 'Next page button',
922+
id: 'be.pagination.nextPageButton',
923+
},
919924
pageStatus: {
920925
defaultMessage: '{pageNumber} of {pageCount}',
921926
description: 'Pagination menu button with current page number out of total number of pages',
922927
id: 'be.pagination.pageStatus',
923928
},
929+
pageEntryStatus: {
930+
defaultMessage: 'Showing {startEntryIndex} to {endEntryIndex} of {totalCount} entries',
931+
description: 'Pagination menu status with the range of entries shown',
932+
id: 'be.pagination.pageEntryStatus',
933+
},
924934
previousFile: {
925935
defaultMessage: 'Previous File',
926936
description: 'Previous file button title',
@@ -931,6 +941,11 @@ const messages = defineMessages({
931941
description: 'Previous page button tooltip',
932942
id: 'be.pagination.previousPage',
933943
},
944+
previousPageButton: {
945+
defaultMessage: 'Previous',
946+
description: 'Previous page button',
947+
id: 'be.pagination.previousPageButton',
948+
},
934949
previousSegment: {
935950
id: 'be.previousSegment',
936951
description: 'Title for previous segment on skill timeline',
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as React from 'react';
2+
import noop from 'lodash/noop';
3+
import PaginationControls from './PaginationControls';
4+
5+
export interface MarkerBasedPaginationProps {
6+
hasNextMarker?: boolean;
7+
hasPrevMarker?: boolean;
8+
isSmall: boolean;
9+
onMarkerBasedPageChange?: (offset: number) => void;
10+
}
11+
12+
const MarkerBasedPagination = ({
13+
hasNextMarker = false,
14+
hasPrevMarker = false,
15+
isSmall,
16+
onMarkerBasedPageChange = noop,
17+
}: MarkerBasedPaginationProps) => {
18+
if (!hasNextMarker && !hasPrevMarker) {
19+
return null;
20+
}
21+
const handleNextClick = () => {
22+
onMarkerBasedPageChange(1);
23+
};
24+
25+
const handlePreviousClick = () => {
26+
onMarkerBasedPageChange(-1);
27+
};
28+
29+
return (
30+
<PaginationControls
31+
handleNextClick={handleNextClick}
32+
handlePreviousClick={handlePreviousClick}
33+
hasNextPage={hasNextMarker}
34+
hasPageEntryStatus={false}
35+
hasPreviousPage={hasPrevMarker}
36+
isSmall={isSmall}
37+
/>
38+
);
39+
};
40+
41+
export default MarkerBasedPagination;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as React from 'react';
2+
import noop from 'lodash/noop';
3+
import PaginationControls from './PaginationControls';
4+
import { DEFAULT_PAGE_SIZE } from '../../../constants';
5+
6+
export interface OffsetBasedPaginationProps {
7+
isSmall: boolean;
8+
offset?: number;
9+
onOffsetChange?: (offset: number) => void;
10+
pageSize?: number;
11+
totalCount?: number;
12+
}
13+
14+
const OffsetBasedPagination = ({
15+
isSmall,
16+
offset = 0,
17+
onOffsetChange = noop,
18+
pageSize = DEFAULT_PAGE_SIZE,
19+
totalCount = 0,
20+
}: OffsetBasedPaginationProps) => {
21+
const pageCount = Math.ceil(totalCount / pageSize);
22+
if (pageCount <= 1) return null;
23+
24+
const pageByOffset = Math.floor(offset / pageSize) + 1;
25+
const pageNumber = pageByOffset > 0 ? Math.min(pageCount, pageByOffset) : 1;
26+
const hasNextPage = pageNumber < pageCount;
27+
const hasPreviousPage = pageNumber > 1;
28+
29+
const updateOffset = (newPageNumber: number) => {
30+
let newOffset = (newPageNumber - 1) * pageSize;
31+
32+
if (newOffset <= 0) {
33+
newOffset = 0;
34+
}
35+
36+
if (newOffset >= totalCount) {
37+
newOffset = totalCount - pageSize;
38+
}
39+
40+
onOffsetChange(newOffset);
41+
};
42+
43+
const handleNextClick = () => {
44+
updateOffset(pageNumber + 1);
45+
};
46+
47+
const handlePreviousClick = () => {
48+
updateOffset(pageNumber - 1);
49+
};
50+
51+
return (
52+
<PaginationControls
53+
handleNextClick={handleNextClick}
54+
handlePreviousClick={handlePreviousClick}
55+
hasNextPage={hasNextPage}
56+
hasPageEntryStatus={true}
57+
hasPreviousPage={hasPreviousPage}
58+
isSmall={isSmall}
59+
offset={offset}
60+
pageSize={pageSize}
61+
totalCount={totalCount}
62+
/>
63+
);
64+
};
65+
66+
export default OffsetBasedPagination;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@import '../../../styles/variables';
2+
3+
.bdl-Pagination {
4+
display: flex;
5+
align-items: center;
6+
justify-content: space-between;
7+
8+
.bdl-Pagination-buttons {
9+
display: flex;
10+
gap: var(--space-2);
11+
12+
.bdl-Pagination-iconButton {
13+
border: 1px solid var(--border-cta-border-secondary);
14+
}
15+
}
16+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react';
2+
import MarkerBasedPagination from './MarkerBasedPagination';
3+
import OffsetBasedPagination from './OffsetBasedPagination';
4+
import './Pagination.scss';
5+
6+
export interface PaginationProps {
7+
hasNextMarker?: boolean;
8+
hasPrevMarker?: boolean;
9+
isSmall: boolean;
10+
offset?: number;
11+
onMarkerBasedPageChange?: (offset: number) => void;
12+
onOffsetChange?: (offset: number) => void;
13+
pageSize?: number;
14+
totalCount?: number;
15+
}
16+
17+
const Pagination = ({ hasNextMarker, hasPrevMarker, isSmall, onMarkerBasedPageChange, ...rest }: PaginationProps) => {
18+
if (hasNextMarker || hasPrevMarker) {
19+
return (
20+
<MarkerBasedPagination
21+
hasNextMarker={hasNextMarker}
22+
hasPrevMarker={hasPrevMarker}
23+
isSmall={isSmall}
24+
onMarkerBasedPageChange={onMarkerBasedPageChange}
25+
/>
26+
);
27+
}
28+
29+
return <OffsetBasedPagination isSmall={isSmall} {...rest} />;
30+
};
31+
32+
export default Pagination;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as React from 'react';
2+
import { FormattedMessage, useIntl } from 'react-intl';
3+
import { Button, IconButton, Tooltip } from '@box/blueprint-web';
4+
import { PointerChevronLeft, PointerChevronRight } from '@box/blueprint-web-assets/icons/Fill';
5+
6+
import messages from '../messages';
7+
8+
import './Pagination.scss';
9+
10+
export interface PaginationControlsProps {
11+
handleNextClick: () => void;
12+
handlePreviousClick: () => void;
13+
hasNextPage: boolean;
14+
hasPageEntryStatus?: boolean;
15+
hasPreviousPage: boolean;
16+
isSmall: boolean;
17+
offset?: number;
18+
pageSize?: number;
19+
totalCount?: number;
20+
}
21+
22+
const PaginationControls = ({
23+
handleNextClick,
24+
handlePreviousClick,
25+
hasNextPage,
26+
hasPageEntryStatus = true,
27+
hasPreviousPage,
28+
isSmall,
29+
offset = 0,
30+
pageSize = 0,
31+
totalCount = 0,
32+
}: PaginationControlsProps) => {
33+
const { formatMessage } = useIntl();
34+
const startEntryIndex = offset + 1;
35+
const endEntryIndex = Math.min(offset + pageSize, totalCount);
36+
37+
return (
38+
<div className="bdl-Pagination">
39+
{hasPageEntryStatus && (
40+
<FormattedMessage
41+
{...messages.pageEntryStatus}
42+
values={{ startEntryIndex, endEntryIndex, totalCount }}
43+
/>
44+
)}
45+
<div className="bdl-Pagination-buttons">
46+
<Tooltip content={formatMessage(messages.previousPage)}>
47+
{isSmall ? (
48+
<IconButton
49+
aria-label={formatMessage(messages.previousPageButton)}
50+
className="bdl-Pagination-iconButton"
51+
disabled={!hasPreviousPage}
52+
icon={PointerChevronLeft}
53+
onClick={handlePreviousClick}
54+
size="large"
55+
/>
56+
) : (
57+
<Button disabled={!hasPreviousPage} onClick={handlePreviousClick} variant="secondary">
58+
{formatMessage(messages.previousPageButton)}
59+
</Button>
60+
)}
61+
</Tooltip>
62+
<Tooltip content={formatMessage(messages.nextPage)}>
63+
{isSmall ? (
64+
<IconButton
65+
aria-label={formatMessage(messages.nextPageButton)}
66+
className="bdl-Pagination-iconButton"
67+
disabled={!hasNextPage}
68+
icon={PointerChevronRight}
69+
onClick={handleNextClick}
70+
size="large"
71+
/>
72+
) : (
73+
<Button disabled={!hasNextPage} onClick={handleNextClick} variant="secondary">
74+
{formatMessage(messages.nextPageButton)}
75+
</Button>
76+
)}
77+
</Tooltip>
78+
</div>
79+
</div>
80+
);
81+
};
82+
83+
export default PaginationControls;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as React from 'react';
2+
import userEvent from '@testing-library/user-event';
3+
import { render, screen } from '../../../../test-utils/testing-library';
4+
import MarkerBasedPagination, { MarkerBasedPaginationProps } from '../MarkerBasedPagination';
5+
6+
describe('elements/common/pagination/MarkerBasedPagination', () => {
7+
const renderComponent = (props: Partial<MarkerBasedPaginationProps> = {}) =>
8+
render(<MarkerBasedPagination isSmall={false} {...props} />);
9+
10+
test('should not render pagination controls when both markers are set to false by default', () => {
11+
renderComponent();
12+
13+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
14+
});
15+
16+
test('should call onMarkerBasedPageChange with correct offset when clicking next', async () => {
17+
const onMarkerBasedPageChange = jest.fn();
18+
renderComponent({
19+
hasNextMarker: true,
20+
onMarkerBasedPageChange,
21+
});
22+
23+
await userEvent.click(screen.getByRole('button', { name: 'Next' }));
24+
expect(onMarkerBasedPageChange).toHaveBeenCalledWith(1);
25+
});
26+
27+
test('should call onMarkerBasedPageChange with correct offset when clicking previous', async () => {
28+
const onMarkerBasedPageChange = jest.fn();
29+
renderComponent({
30+
hasPrevMarker: true,
31+
onMarkerBasedPageChange,
32+
});
33+
34+
await userEvent.click(screen.getByRole('button', { name: 'Previous' }));
35+
expect(onMarkerBasedPageChange).toHaveBeenCalledWith(-1);
36+
});
37+
});

0 commit comments

Comments
 (0)