Skip to content

Commit 34eb950

Browse files
authoredJun 2, 2021
feat(ts): convert pagination widget and component (#4765)
1 parent 3648a3c commit 34eb950

File tree

9 files changed

+706
-569
lines changed

9 files changed

+706
-569
lines changed
 

‎src/components/Pagination/Pagination.js

-174
This file was deleted.
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/** @jsx h */
2+
3+
import { Component, h } from 'preact';
4+
import cx from 'classnames';
5+
6+
import PaginationLink from './PaginationLink';
7+
import { isSpecialClick } from '../../lib/utils';
8+
import {
9+
PaginationCSSClasses,
10+
PaginationTemplates,
11+
} from '../../widgets/pagination/pagination';
12+
13+
export type PaginationComponentCSSClasses = {
14+
[TClassName in keyof PaginationCSSClasses]: string;
15+
};
16+
17+
export type PaginationProps = {
18+
createURL(value: number): string;
19+
cssClasses: PaginationComponentCSSClasses;
20+
currentPage: number;
21+
templates: PaginationTemplates;
22+
nbPages?: number;
23+
pages?: number[];
24+
isFirstPage: boolean;
25+
isLastPage: boolean;
26+
setCurrentPage(value: number): void;
27+
showFirst?: boolean;
28+
showLast?: boolean;
29+
showPrevious?: boolean;
30+
showNext?: boolean;
31+
};
32+
33+
type PageLink = {
34+
label: string;
35+
ariaLabel: string;
36+
pageNumber: number;
37+
additionalClassName: string | null;
38+
isDisabled?: boolean;
39+
isSelected?: boolean;
40+
createURL(value: number): string;
41+
};
42+
43+
const defaultProps = {
44+
currentPage: 0,
45+
nbPages: 0,
46+
pages: [],
47+
};
48+
49+
class Pagination extends Component<PaginationProps> {
50+
public static defaultProps = defaultProps;
51+
52+
private pageLink({
53+
label,
54+
ariaLabel,
55+
pageNumber,
56+
additionalClassName = null,
57+
isDisabled = false,
58+
isSelected = false,
59+
createURL,
60+
}: PageLink) {
61+
const cssClasses = {
62+
item: cx(this.props.cssClasses.item, additionalClassName),
63+
link: this.props.cssClasses.link,
64+
};
65+
66+
if (isDisabled) {
67+
cssClasses.item = cx(cssClasses.item, this.props.cssClasses.disabledItem);
68+
} else if (isSelected) {
69+
cssClasses.item = cx(cssClasses.item, this.props.cssClasses.selectedItem);
70+
}
71+
72+
const url = !isDisabled ? createURL(pageNumber) : '#';
73+
74+
return (
75+
<PaginationLink
76+
ariaLabel={ariaLabel}
77+
cssClasses={cssClasses}
78+
handleClick={this.handleClick}
79+
isDisabled={isDisabled}
80+
key={label + pageNumber + ariaLabel}
81+
label={label}
82+
pageNumber={pageNumber}
83+
url={url}
84+
/>
85+
);
86+
}
87+
88+
public handleClick = (pageNumber: number, event: MouseEvent) => {
89+
if (isSpecialClick(event)) {
90+
// do not alter the default browser behavior
91+
// if one special key is down
92+
return;
93+
}
94+
event.preventDefault();
95+
this.props.setCurrentPage(pageNumber);
96+
};
97+
98+
private previousPageLink = () => {
99+
return this.pageLink({
100+
ariaLabel: 'Previous',
101+
additionalClassName: this.props.cssClasses.previousPageItem,
102+
isDisabled: this.props.isFirstPage,
103+
label: this.props.templates.previous,
104+
pageNumber: this.props.currentPage - 1,
105+
createURL: this.props.createURL,
106+
});
107+
};
108+
109+
private nextPageLink = () => {
110+
return this.pageLink({
111+
ariaLabel: 'Next',
112+
additionalClassName: this.props.cssClasses.nextPageItem,
113+
isDisabled: this.props.isLastPage,
114+
label: this.props.templates.next,
115+
pageNumber: this.props.currentPage + 1,
116+
createURL: this.props.createURL,
117+
});
118+
};
119+
120+
private firstPageLink = () => {
121+
return this.pageLink({
122+
ariaLabel: 'First',
123+
additionalClassName: this.props.cssClasses.firstPageItem,
124+
isDisabled: this.props.isFirstPage,
125+
label: this.props.templates.first,
126+
pageNumber: 0,
127+
createURL: this.props.createURL,
128+
});
129+
};
130+
131+
private lastPageLink = () => {
132+
return this.pageLink({
133+
ariaLabel: 'Last',
134+
additionalClassName: this.props.cssClasses.lastPageItem,
135+
isDisabled: this.props.isLastPage,
136+
label: this.props.templates.last,
137+
pageNumber: this.props.nbPages! - 1,
138+
createURL: this.props.createURL,
139+
});
140+
};
141+
142+
private pages = () => {
143+
return this.props.pages!.map(pageNumber =>
144+
this.pageLink({
145+
ariaLabel: `${pageNumber + 1}`,
146+
additionalClassName: this.props.cssClasses.pageItem,
147+
isSelected: pageNumber === this.props.currentPage,
148+
label: `${pageNumber + 1}`,
149+
pageNumber,
150+
createURL: this.props.createURL,
151+
})
152+
);
153+
};
154+
155+
public render() {
156+
return (
157+
<div
158+
className={cx(this.props.cssClasses.root, {
159+
[this.props.cssClasses.noRefinementRoot]: this.props.nbPages! <= 1,
160+
})}
161+
>
162+
<ul className={this.props.cssClasses.list}>
163+
{this.props.showFirst && this.firstPageLink()}
164+
{this.props.showPrevious && this.previousPageLink()}
165+
{this.pages()}
166+
{this.props.showNext && this.nextPageLink()}
167+
{this.props.showLast && this.lastPageLink()}
168+
</ul>
169+
</div>
170+
);
171+
}
172+
}
173+
174+
export default Pagination;

‎src/components/Pagination/PaginationLink.js ‎src/components/Pagination/PaginationLink.tsx

+17-17
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
/** @jsx h */
22

33
import { h } from 'preact';
4-
import PropTypes from 'prop-types';
54

6-
function PaginationLink({
5+
export type PaginationLinkCSSClasses = {
6+
item: string;
7+
link: string;
8+
};
9+
10+
export type PaginationLinkProps = {
11+
ariaLabel: string;
12+
cssClasses: PaginationLinkCSSClasses;
13+
handleClick(pageNumber: number, event: MouseEvent): void;
14+
isDisabled: boolean;
15+
label: string;
16+
pageNumber: number;
17+
url?: string;
18+
};
19+
20+
const PaginationLink = ({
721
cssClasses,
822
label,
923
ariaLabel,
1024
url,
1125
isDisabled,
1226
handleClick,
1327
pageNumber,
14-
}) {
28+
}: PaginationLinkProps) => {
1529
if (isDisabled) {
1630
return (
1731
<li className={cssClasses.item}>
@@ -38,20 +52,6 @@ function PaginationLink({
3852
/>
3953
</li>
4054
);
41-
}
42-
43-
PaginationLink.propTypes = {
44-
ariaLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
45-
.isRequired,
46-
cssClasses: PropTypes.shape({
47-
item: PropTypes.string.isRequired,
48-
link: PropTypes.string.isRequired,
49-
}).isRequired,
50-
handleClick: PropTypes.func.isRequired,
51-
isDisabled: PropTypes.bool,
52-
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
53-
pageNumber: PropTypes.number,
54-
url: PropTypes.string,
5555
};
5656

5757
export default PaginationLink;

‎src/components/Pagination/__tests__/Pagination-test.js ‎src/components/Pagination/__tests__/Pagination-test.tsx

+43-33
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22

33
import { h } from 'preact';
44
import { mount } from 'enzyme';
5-
import Pagination from '../Pagination';
5+
import Pagination, { PaginationProps } from '../Pagination';
66
import Paginator from '../../../connectors/pagination/Paginator';
7+
import { ReactElementLike } from 'prop-types';
78

89
describe('Pagination', () => {
910
const pager = new Paginator({
1011
currentPage: 0,
1112
total: 20,
1213
padding: 3,
1314
});
14-
const defaultProps = {
15+
const defaultProps: PaginationProps = {
1516
cssClasses: {
1617
root: 'root',
1718
noRefinementRoot: 'noRefinementRoot',
@@ -26,52 +27,58 @@ describe('Pagination', () => {
2627
disabledItem: 'disabledItem',
2728
link: 'link',
2829
},
29-
createURL: (...args) => JSON.stringify(args),
30+
createURL: args => JSON.stringify(args),
3031
templates: { first: '', last: '', next: '', previous: '' },
3132
currentPage: 0,
32-
nbHits: 200,
3333
pages: pager.pages(),
3434
isFirstPage: pager.isFirstPage(),
3535
isLastPage: pager.isLastPage(),
3636
nbPages: 20,
37-
padding: 3,
3837
setCurrentPage: () => {},
3938
};
4039

4140
it('should render five elements', () => {
42-
const wrapper = mount(<Pagination {...defaultProps} />);
41+
const wrapper = mount(
42+
(<Pagination {...defaultProps} />) as ReactElementLike
43+
);
4344

4445
expect(wrapper).toMatchSnapshot();
4546
});
4647

4748
it('should display the first/last link', () => {
48-
const wrapper = mount(<Pagination {...defaultProps} showFirst showLast />);
49+
const wrapper = mount(
50+
(<Pagination {...defaultProps} showFirst showLast />) as ReactElementLike
51+
);
4952

5053
expect(wrapper.find('.firstPageItem')).toHaveLength(1);
5154
expect(wrapper.find('.lastPageItem')).toHaveLength(1);
5255
expect(wrapper).toMatchSnapshot();
5356
});
5457

5558
it('should add the noRefinement CSS class with a single page', () => {
56-
const wrapper = mount(<Pagination {...defaultProps} nbPages={1} />);
59+
const wrapper = mount(
60+
(<Pagination {...defaultProps} nbPages={1} />) as ReactElementLike
61+
);
5762

5863
expect(wrapper.find('.noRefinementRoot')).toHaveLength(1);
5964
expect(wrapper).toMatchSnapshot();
6065
});
6166

6267
it('should disable last page if already on it', () => {
6368
const wrapper = mount(
64-
<Pagination
65-
{...defaultProps}
66-
showFirst
67-
showLast
68-
showPrevious
69-
showNext
70-
pages={[13, 14, 15, 16, 17, 18, 19]}
71-
currentPage={19}
72-
isFirstPage={false}
73-
isLastPage={true}
74-
/>
69+
(
70+
<Pagination
71+
{...defaultProps}
72+
showFirst
73+
showLast
74+
showPrevious
75+
showNext
76+
pages={[13, 14, 15, 16, 17, 18, 19]}
77+
currentPage={19}
78+
isFirstPage={false}
79+
isLastPage={true}
80+
/>
81+
) as ReactElementLike
7582
);
7683

7784
expect(wrapper.find('.lastPageItem').hasClass('disabledItem')).toBe(true);
@@ -83,15 +90,17 @@ describe('Pagination', () => {
8390
setCurrentPage: jest.fn(),
8491
};
8592
const preventDefault = jest.fn();
86-
const component = new Pagination(props);
93+
const component = new Pagination({ ...defaultProps, ...props });
8794
['ctrlKey', 'shiftKey', 'altKey', 'metaKey'].forEach(e => {
8895
const event = { preventDefault };
8996
event[e] = true;
97+
// @ts-expect-error
9098
component.handleClick(42, event);
9199

92100
expect(props.setCurrentPage).toHaveBeenCalledTimes(0);
93101
expect(preventDefault).toHaveBeenCalledTimes(0);
94102
});
103+
// @ts-expect-error
95104
component.handleClick(42, { preventDefault });
96105

97106
expect(props.setCurrentPage).toHaveBeenCalledTimes(1);
@@ -105,19 +114,20 @@ describe('Pagination', () => {
105114
padding: 3,
106115
});
107116
const wrapper = mount(
108-
<Pagination
109-
{...defaultProps}
110-
showFirst
111-
showLast
112-
showPrevious
113-
showNext
114-
currentPage={0}
115-
nbHits={0}
116-
nbPages={0}
117-
pages={localPager.pages()}
118-
isFirstPage={localPager.isFirstPage()}
119-
isLastPage={localPager.isLastPage()}
120-
/>
117+
(
118+
<Pagination
119+
{...defaultProps}
120+
showFirst
121+
showLast
122+
showPrevious
123+
showNext
124+
currentPage={0}
125+
nbPages={0}
126+
pages={localPager.pages()}
127+
isFirstPage={localPager.isFirstPage()}
128+
isLastPage={localPager.isLastPage()}
129+
/>
130+
) as ReactElementLike
121131
);
122132

123133
expect(wrapper).toMatchSnapshot();

‎src/components/Pagination/__tests__/__snapshots__/Pagination-test.js.snap ‎src/components/Pagination/__tests__/__snapshots__/Pagination-test.tsx.snap

+90-90
Large diffs are not rendered by default.

‎src/widgets/pagination/__tests__/__snapshots__/pagination-test.js.snap ‎src/widgets/pagination/__tests__/__snapshots__/pagination-test.ts.snap

-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ Object {
2020
"currentPage": 0,
2121
"isFirstPage": true,
2222
"isLastPage": false,
23-
"nbHits": 200,
2423
"nbPages": 20,
2524
"pages": Array [
2625
0,
@@ -42,7 +41,6 @@ Object {
4241
"next": "",
4342
"previous": "",
4443
},
45-
"totalPages": undefined,
4644
}
4745
`;
4846

@@ -66,7 +64,6 @@ Object {
6664
"currentPage": 0,
6765
"isFirstPage": true,
6866
"isLastPage": false,
69-
"nbHits": 200,
7067
"nbPages": 20,
7168
"pages": Array [
7269
0,
@@ -88,6 +85,5 @@ Object {
8885
"next": "",
8986
"previous": "",
9087
},
91-
"totalPages": undefined,
9288
}
9389
`;

‎src/widgets/pagination/__tests__/pagination-test.js ‎src/widgets/pagination/__tests__/pagination-test.ts

+67-23
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
import { render } from 'preact';
2-
import getContainerNode from '../../../lib/utils/getContainerNode';
3-
import pagination from '../pagination';
4-
import { createRenderOptions } from '../../../../test/mock/createWidget';
5-
import { SearchResults, SearchParameters } from 'algoliasearch-helper';
1+
import { render as preactRender } from 'preact';
2+
import utilsGetContainerNode from '../../../lib/utils/getContainerNode';
3+
import pagination, {
4+
PaginationCSSClasses,
5+
PaginationWidgetParams,
6+
} from '../pagination';
7+
import {
8+
createInitOptions,
9+
createRenderOptions,
10+
} from '../../../../test/mock/createWidget';
11+
import algoliasearchHelper, {
12+
SearchResults,
13+
SearchParameters,
14+
} from 'algoliasearch-helper';
615
import { createSingleSearchResponse } from '../../../../test/mock/createAPIResponse';
16+
import { castToJestMock } from '../../../../test/utils/castToJestMock';
17+
import { createSearchClient } from '../../../../test/mock/createSearchClient';
718

19+
const render = castToJestMock(preactRender);
820
jest.mock('preact', () => {
921
const module = jest.requireActual('preact');
1022

@@ -13,6 +25,7 @@ jest.mock('preact', () => {
1325
return module;
1426
});
1527

28+
const getContainerNode = castToJestMock(utilsGetContainerNode);
1629
jest.mock('../../../lib/utils/getContainerNode', () => {
1730
const module = jest.requireActual('../../../lib/utils/getContainerNode');
1831

@@ -25,6 +38,7 @@ jest.mock('../../../lib/utils/getContainerNode', () => {
2538
describe('Usage', () => {
2639
it('throws without container', () => {
2740
expect(() => {
41+
// @ts-expect-error
2842
pagination({ container: undefined });
2943
}).toThrowErrorMatchingInlineSnapshot(`
3044
"The \`container\` option is required.
@@ -35,11 +49,11 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/pagination/
3549
});
3650

3751
describe('pagination()', () => {
38-
let container;
39-
let widget;
40-
let results;
52+
let widget: ReturnType<typeof pagination>;
53+
let container: HTMLElement;
4154
let helper;
42-
let cssClasses;
55+
let results;
56+
let cssClasses: PaginationCSSClasses;
4357

4458
beforeEach(() => {
4559
render.mockClear();
@@ -72,54 +86,84 @@ describe('pagination()', () => {
7286
search: jest.fn(),
7387
state: {},
7488
};
75-
widget.init({ helper });
89+
widget.init!(createInitOptions({ helper }));
7690
});
7791

7892
it('sets the page', () => {
79-
widget.getWidgetRenderState({ helper }).refine(42);
93+
widget.getWidgetRenderState(createInitOptions({ helper })).refine(42);
8094
expect(helper.setPage).toHaveBeenCalledTimes(1);
8195
expect(helper.search).toHaveBeenCalledTimes(1);
8296
});
8397

8498
it('calls twice render(<Pagination props />, container)', () => {
85-
widget.render({ results, helper, state: { page: 0 } });
86-
widget.render({ results, helper, state: { page: 0 } });
99+
const { state } = algoliasearchHelper(createSearchClient(), '', {
100+
page: 0,
101+
});
102+
103+
widget.render!(
104+
createRenderOptions({
105+
results,
106+
helper,
107+
state,
108+
})
109+
);
110+
widget.render!(
111+
createRenderOptions({
112+
results,
113+
helper,
114+
state,
115+
})
116+
);
87117

88118
const [firstRender, secondRender] = render.mock.calls;
89119

90120
expect(render).toHaveBeenCalledTimes(2);
121+
// @ts-expect-error
91122
expect(firstRender[0].props).toMatchSnapshot();
92123
expect(firstRender[1]).toEqual(container);
124+
// @ts-expect-error
93125
expect(secondRender[0].props).toMatchSnapshot();
94126
expect(secondRender[1]).toEqual(container);
95127
});
96128

97129
describe('mocking getContainerNode', () => {
98-
let scrollIntoView;
130+
let scrollIntoView: jest.Mock;
99131

100132
beforeEach(() => {
101133
scrollIntoView = jest.fn();
102134
});
103135

104136
it('should not scroll', () => {
105137
widget = pagination({ container, scrollTo: false });
106-
widget.init({ helper });
107-
widget.getWidgetRenderState({ helper }).refine(2);
138+
widget.init!(createInitOptions({ helper }));
139+
widget.getWidgetRenderState(createInitOptions({ helper })).refine(2);
108140
expect(scrollIntoView).toHaveBeenCalledTimes(0);
109141
});
110142

111-
it('should scroll to body', () => {
143+
it('should scrollto body', () => {
144+
const { state } = algoliasearchHelper(createSearchClient(), '', {
145+
page: 0,
146+
});
147+
148+
// @ts-expect-error
112149
getContainerNode.mockImplementation(input =>
113150
input === 'body' ? { scrollIntoView } : input
114151
);
115152

116153
widget = pagination({ container });
117154

118-
widget.init({ helper });
119-
widget.render({ results, helper, state: { page: 0 } });
155+
widget.init!(createInitOptions({ helper }));
156+
widget.render!(
157+
createRenderOptions({
158+
results,
159+
helper,
160+
state,
161+
})
162+
);
120163

121164
const [firstRender] = render.mock.calls;
122165

166+
// @ts-expect-error
123167
firstRender[0].props.setCurrentPage(2);
124168

125169
expect(scrollIntoView).toHaveBeenCalledTimes(1);
@@ -128,11 +172,11 @@ describe('pagination()', () => {
128172
});
129173

130174
describe('pagination MaxPage', () => {
131-
let container;
132-
let widget;
175+
let widget: ReturnType<typeof pagination>;
176+
let container: HTMLElement;
133177
let results;
134-
let cssClasses;
135-
let paginationOptions;
178+
let cssClasses: PaginationCSSClasses;
179+
let paginationOptions: PaginationWidgetParams;
136180

137181
beforeEach(() => {
138182
container = document.createElement('div');

‎src/widgets/pagination/pagination.js

-228
This file was deleted.

‎src/widgets/pagination/pagination.tsx

+315
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/** @jsx h */
2+
3+
import { h, render } from 'preact';
4+
import cx from 'classnames';
5+
import Pagination from '../../components/Pagination/Pagination';
6+
import connectPagination, {
7+
PaginationConnectorParams,
8+
PaginationRenderState,
9+
PaginationWidgetDescription,
10+
} from '../../connectors/pagination/connectPagination';
11+
import {
12+
getContainerNode,
13+
createDocumentationMessageGenerator,
14+
} from '../../lib/utils';
15+
import { component } from '../../lib/suit';
16+
import { Renderer, WidgetFactory } from '../../types';
17+
18+
const suit = component('Pagination');
19+
const withUsage = createDocumentationMessageGenerator({ name: 'pagination' });
20+
21+
const defaultTemplates: PaginationTemplates = {
22+
previous: '‹',
23+
next: '›',
24+
first: '«',
25+
last: '»',
26+
};
27+
28+
const renderer = ({
29+
containerNode,
30+
cssClasses,
31+
templates,
32+
showFirst,
33+
showLast,
34+
showPrevious,
35+
showNext,
36+
scrollToNode,
37+
}): Renderer<PaginationRenderState, Partial<PaginationWidgetParams>> => (
38+
{
39+
createURL,
40+
currentRefinement,
41+
nbPages,
42+
pages,
43+
isFirstPage,
44+
isLastPage,
45+
refine,
46+
},
47+
isFirstRendering
48+
) => {
49+
if (isFirstRendering) return;
50+
51+
const setCurrentPage = (pageNumber: number) => {
52+
refine(pageNumber);
53+
54+
if (scrollToNode !== false) {
55+
scrollToNode.scrollIntoView();
56+
}
57+
};
58+
59+
render(
60+
<Pagination
61+
createURL={createURL}
62+
cssClasses={cssClasses}
63+
currentPage={currentRefinement}
64+
templates={templates}
65+
nbPages={nbPages}
66+
pages={pages}
67+
isFirstPage={isFirstPage}
68+
isLastPage={isLastPage}
69+
setCurrentPage={setCurrentPage}
70+
showFirst={showFirst}
71+
showLast={showLast}
72+
showPrevious={showPrevious}
73+
showNext={showNext}
74+
/>,
75+
containerNode
76+
);
77+
};
78+
79+
export type PaginationCSSClasses = {
80+
/**
81+
* CSS classes added to the root element of the widget.
82+
*/
83+
root: string | string[];
84+
85+
/**
86+
* CSS class to add to the root element of the widget if there are no refinements.
87+
*/
88+
noRefinementRoot: string | string[];
89+
90+
/**
91+
* CSS classes added to the wrapping `<ul>`.
92+
*/
93+
list: string | string[];
94+
95+
/**
96+
* CSS classes added to each `<li>`.
97+
*/
98+
item: string | string[];
99+
100+
/**
101+
* CSS classes added to the first `<li>`.
102+
*/
103+
firstPageItem: string | string[];
104+
105+
/**
106+
* CSS classes added to the last `<li>`.
107+
*/
108+
lastPageItem: string | string[];
109+
110+
/**
111+
* CSS classes added to the previous `<li>`.
112+
*/
113+
previousPageItem: string | string[];
114+
115+
/**
116+
* CSS classes added to the next `<li>`.
117+
*/
118+
nextPageItem: string | string[];
119+
120+
/**
121+
* CSS classes added to page `<li>`.
122+
*/
123+
pageItem: string | string[];
124+
125+
/**
126+
* CSS classes added to the selected `<li>`.
127+
*/
128+
selectedItem: string | string[];
129+
130+
/**
131+
* CSS classes added to the disabled `<li>`.
132+
*/
133+
disabledItem: string | string[];
134+
135+
/**
136+
* CSS classes added to each link.
137+
*/
138+
link: string | string[];
139+
};
140+
141+
export type PaginationTemplates = {
142+
/**
143+
* Label for the Previous link.
144+
*/
145+
previous: string;
146+
147+
/**
148+
* Label for the Next link.
149+
*/
150+
next: string;
151+
152+
/**
153+
* Label for the First link.
154+
*/
155+
first: string;
156+
157+
/**
158+
* Label for the Last link.
159+
*/
160+
last: string;
161+
};
162+
163+
export type PaginationWidgetParams = {
164+
/**
165+
* CSS Selector or HTMLElement to insert the widget.
166+
*/
167+
container: string | HTMLElement;
168+
169+
/**
170+
* The max number of pages to browse.
171+
*/
172+
totalPages?: number;
173+
174+
/**
175+
* The number of pages to display on each side of the current page.
176+
* @default 3
177+
*/
178+
padding?: number;
179+
180+
/**
181+
* Where to scroll after a click, set to `false` to disable.
182+
* @default body
183+
*/
184+
scrollTo?: string | HTMLElement | boolean;
185+
186+
/**
187+
* Whether to show the "first page" control
188+
* @default true
189+
*/
190+
showFirst?: boolean;
191+
192+
/**
193+
* Whether to show the "last page" control
194+
* @default true
195+
*/
196+
showLast?: boolean;
197+
198+
/**
199+
* Whether to show the "next page" control
200+
* @default true
201+
*/
202+
showNext?: boolean;
203+
204+
/**
205+
* Whether to show the "previous page" control
206+
* @default true
207+
*/
208+
showPrevious?: boolean;
209+
210+
/**
211+
* Text to display in the links.
212+
*/
213+
templates?: Partial<PaginationTemplates>;
214+
215+
/**
216+
* CSS classes to be added.
217+
*/
218+
cssClasses?: Partial<PaginationCSSClasses>;
219+
};
220+
221+
export type PaginationWidget = WidgetFactory<
222+
PaginationWidgetDescription & { $$widgetType: 'ais.pagination' },
223+
PaginationConnectorParams,
224+
PaginationWidgetParams
225+
>;
226+
227+
const pagination: PaginationWidget = function pagination(widgetParams) {
228+
const {
229+
container,
230+
templates: userTemplates = {},
231+
cssClasses: userCssClasses = {},
232+
totalPages,
233+
padding,
234+
showFirst = true,
235+
showLast = true,
236+
showPrevious = true,
237+
showNext = true,
238+
scrollTo: userScrollTo = 'body',
239+
} = widgetParams || {};
240+
241+
if (!container) {
242+
throw new Error(withUsage('The `container` option is required.'));
243+
}
244+
245+
const containerNode = getContainerNode(container);
246+
247+
const scrollTo = userScrollTo === true ? 'body' : userScrollTo;
248+
const scrollToNode = scrollTo !== false ? getContainerNode(scrollTo) : false;
249+
250+
const cssClasses = {
251+
root: cx(suit(), userCssClasses.root),
252+
noRefinementRoot: cx(
253+
suit({ modifierName: 'noRefinement' }),
254+
userCssClasses.noRefinementRoot
255+
),
256+
list: cx(suit({ descendantName: 'list' }), userCssClasses.list),
257+
item: cx(suit({ descendantName: 'item' }), userCssClasses.item),
258+
firstPageItem: cx(
259+
suit({ descendantName: 'item', modifierName: 'firstPage' }),
260+
userCssClasses.firstPageItem
261+
),
262+
lastPageItem: cx(
263+
suit({ descendantName: 'item', modifierName: 'lastPage' }),
264+
userCssClasses.lastPageItem
265+
),
266+
previousPageItem: cx(
267+
suit({ descendantName: 'item', modifierName: 'previousPage' }),
268+
userCssClasses.previousPageItem
269+
),
270+
nextPageItem: cx(
271+
suit({ descendantName: 'item', modifierName: 'nextPage' }),
272+
userCssClasses.nextPageItem
273+
),
274+
pageItem: cx(
275+
suit({ descendantName: 'item', modifierName: 'page' }),
276+
userCssClasses.pageItem
277+
),
278+
selectedItem: cx(
279+
suit({ descendantName: 'item', modifierName: 'selected' }),
280+
userCssClasses.selectedItem
281+
),
282+
disabledItem: cx(
283+
suit({ descendantName: 'item', modifierName: 'disabled' }),
284+
userCssClasses.disabledItem
285+
),
286+
link: cx(suit({ descendantName: 'link' }), userCssClasses.link),
287+
};
288+
289+
const templates: PaginationTemplates = {
290+
...defaultTemplates,
291+
...userTemplates,
292+
};
293+
294+
const specializedRenderer = renderer({
295+
containerNode,
296+
cssClasses,
297+
templates,
298+
showFirst,
299+
showLast,
300+
showPrevious,
301+
showNext,
302+
scrollToNode,
303+
});
304+
305+
const makeWidget = connectPagination(specializedRenderer, () =>
306+
render(null, containerNode)
307+
);
308+
309+
return {
310+
...makeWidget({ totalPages, padding }),
311+
$$widgetType: 'ais.pagination',
312+
};
313+
};
314+
315+
export default pagination;

0 commit comments

Comments
 (0)
Please sign in to comment.