Skip to content
This repository was archived by the owner on Dec 30, 2022. It is now read-only.

Commit f5e63fe

Browse files
feat(hooks): implement <HitsPerPage> (#3383)
1 parent b96809e commit f5e63fe

File tree

9 files changed

+326
-36
lines changed

9 files changed

+326
-36
lines changed

examples/hooks/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CurrentRefinements,
99
DynamicWidgets,
1010
Hits,
11+
HitsPerPage,
1112
Pagination,
1213
SearchBox,
1314
SortBy,
@@ -17,7 +18,6 @@ import {
1718
Breadcrumb,
1819
HierarchicalMenu,
1920
Highlight,
20-
HitsPerPage,
2121
InfiniteHits,
2222
Menu,
2323
NumericMenu,

examples/hooks/components/HitsPerPage.tsx

Lines changed: 0 additions & 34 deletions
This file was deleted.

examples/hooks/components/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export * from './Highlight';
44
export * from './InfiniteHits';
55
export * from './Menu';
66
export * from './NumericMenu';
7-
export * from './HitsPerPage';
87
export * from './Panel';
98
export * from './PoweredBy';
109
export * from './QueryRuleContext';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
3+
import { cx } from './lib/cx';
4+
5+
import type { HitsPerPageConnectorParamsItem as HitsPerPageItem } from 'instantsearch.js/es/connectors/hits-per-page/connectHitsPerPage';
6+
7+
export type HitsPerPageProps = React.HTMLAttributes<HTMLDivElement> & {
8+
items: HitsPerPageItem[];
9+
onChange: (value: number) => void;
10+
currentValue: number;
11+
};
12+
13+
export function HitsPerPage({
14+
items,
15+
onChange,
16+
currentValue,
17+
...props
18+
}: HitsPerPageProps) {
19+
return (
20+
<div {...props} className={cx('ais-HitsPerPage', props.className)}>
21+
<select
22+
className="ais-HitsPerPage-select"
23+
onChange={(event) => {
24+
onChange(Number(event.target.value));
25+
}}
26+
value={String(currentValue)}
27+
>
28+
{items.map((item) => (
29+
<option
30+
key={item.value}
31+
className="ais-HitsPerPage-option"
32+
value={item.value}
33+
>
34+
{item.label}
35+
</option>
36+
))}
37+
</select>
38+
</div>
39+
);
40+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { render } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import React from 'react';
4+
5+
import { HitsPerPage } from '../HitsPerPage';
6+
7+
import type { HitsPerPageProps } from '../HitsPerPage';
8+
9+
function createHitsPerPageProps(
10+
props?: Partial<HitsPerPageProps>
11+
): HitsPerPageProps {
12+
return {
13+
items: [
14+
{ label: '10', value: 10, default: true },
15+
{ label: '20', value: 20 },
16+
{ label: '30', value: 30 },
17+
],
18+
currentValue: 10,
19+
onChange: jest.fn(),
20+
...props,
21+
};
22+
}
23+
24+
describe('HitsPerPage', () => {
25+
test('renders with items', () => {
26+
const props = createHitsPerPageProps();
27+
const { container } = render(<HitsPerPage {...props} />);
28+
29+
expect(container).toMatchInlineSnapshot(`
30+
<div>
31+
<div
32+
class="ais-HitsPerPage"
33+
>
34+
<select
35+
class="ais-HitsPerPage-select"
36+
>
37+
<option
38+
class="ais-HitsPerPage-option"
39+
value="10"
40+
>
41+
10
42+
</option>
43+
<option
44+
class="ais-HitsPerPage-option"
45+
value="20"
46+
>
47+
20
48+
</option>
49+
<option
50+
class="ais-HitsPerPage-option"
51+
value="30"
52+
>
53+
30
54+
</option>
55+
</select>
56+
</div>
57+
</div>
58+
`);
59+
});
60+
61+
test('forwards props to the root element', () => {
62+
const props = createHitsPerPageProps({
63+
title: 'Some custom title',
64+
className: 'MyHitsPerPage',
65+
});
66+
const { container } = render(<HitsPerPage {...props} />);
67+
const root = container.firstChild;
68+
69+
expect(root).toHaveClass('ais-HitsPerPage', 'MyHitsPerPage');
70+
expect(root).toHaveAttribute('title', 'Some custom title');
71+
});
72+
73+
test('selects current value', () => {
74+
const props = createHitsPerPageProps({
75+
currentValue: 20,
76+
});
77+
const { getByRole } = render(<HitsPerPage {...props} />);
78+
79+
expect(
80+
(getByRole('option', { name: '10' }) as HTMLOptionElement).selected
81+
).toBe(false);
82+
expect(
83+
(getByRole('option', { name: '20' }) as HTMLOptionElement).selected
84+
).toBe(true);
85+
expect(
86+
(getByRole('option', { name: '30' }) as HTMLOptionElement).selected
87+
).toBe(false);
88+
});
89+
90+
test('calls `onChange` when selecting an option', () => {
91+
const props = createHitsPerPageProps();
92+
const { getByRole } = render(<HitsPerPage {...props} />);
93+
94+
userEvent.selectOptions(getByRole('combobox'), ['10']);
95+
96+
expect(props.onChange).toHaveBeenCalledTimes(1);
97+
expect(props.onChange).toHaveBeenLastCalledWith(10);
98+
99+
userEvent.selectOptions(getByRole('combobox'), ['20']);
100+
101+
expect(props.onChange).toHaveBeenCalledTimes(2);
102+
expect(props.onChange).toHaveBeenLastCalledWith(20);
103+
});
104+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { useHitsPerPage } from 'react-instantsearch-hooks';
3+
4+
import { HitsPerPage as HitsPerPageUiComponent } from '../ui/HitsPerPage';
5+
6+
import type { UseHitsPerPageProps } from 'react-instantsearch-hooks';
7+
8+
export type HitsPerPageProps = React.HTMLAttributes<HTMLDivElement> &
9+
UseHitsPerPageProps;
10+
11+
export function HitsPerPage(props: HitsPerPageProps) {
12+
const { items, refine } = useHitsPerPage(props, {
13+
$$widgetType: 'ais.hitsPerPage',
14+
});
15+
const { value: currentValue } =
16+
items.find(({ isRefined }) => isRefined)! || {};
17+
18+
return (
19+
<HitsPerPageUiComponent
20+
{...props}
21+
items={items}
22+
currentValue={currentValue}
23+
onChange={(value) => {
24+
refine(value);
25+
}}
26+
/>
27+
);
28+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { render } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import React from 'react';
4+
5+
import { createSearchClient } from '../../../../../test/mock';
6+
import { InstantSearchHooksTestWrapper, wait } from '../../../../../test/utils';
7+
import { HitsPerPage } from '../HitsPerPage';
8+
9+
describe('HitsPerPage', () => {
10+
test('renders with items', async () => {
11+
const { container } = render(
12+
<InstantSearchHooksTestWrapper>
13+
<HitsPerPage
14+
items={[
15+
{ label: '10', value: 10, default: true },
16+
{ label: '20', value: 20 },
17+
{ label: '30', value: 30 },
18+
]}
19+
/>
20+
</InstantSearchHooksTestWrapper>
21+
);
22+
23+
await wait(0);
24+
25+
expect(container).toMatchInlineSnapshot(`
26+
<div>
27+
<div
28+
class="ais-HitsPerPage"
29+
>
30+
<select
31+
class="ais-HitsPerPage-select"
32+
>
33+
<option
34+
class="ais-HitsPerPage-option"
35+
value="10"
36+
>
37+
10
38+
</option>
39+
<option
40+
class="ais-HitsPerPage-option"
41+
value="20"
42+
>
43+
20
44+
</option>
45+
<option
46+
class="ais-HitsPerPage-option"
47+
value="30"
48+
>
49+
30
50+
</option>
51+
</select>
52+
</div>
53+
</div>
54+
`);
55+
});
56+
57+
test('forwards props to the root element', async () => {
58+
const { container } = render(
59+
<InstantSearchHooksTestWrapper>
60+
<HitsPerPage
61+
className="MyHitsPerPage"
62+
title="Some custom title"
63+
items={[
64+
{ label: '10', value: 10, default: true },
65+
{ label: '20', value: 20 },
66+
{ label: '30', value: 30 },
67+
]}
68+
/>
69+
</InstantSearchHooksTestWrapper>
70+
);
71+
const root = container.firstChild;
72+
73+
await wait(0);
74+
75+
expect(root).toHaveClass('ais-HitsPerPage', 'MyHitsPerPage');
76+
expect(root).toHaveAttribute('title', 'Some custom title');
77+
});
78+
79+
test('selects current value', async () => {
80+
const { getByRole } = render(
81+
<InstantSearchHooksTestWrapper
82+
initialUiState={{
83+
indexName: {
84+
hitsPerPage: 20,
85+
},
86+
}}
87+
>
88+
<HitsPerPage
89+
items={[
90+
{ label: '10', value: 10, default: true },
91+
{ label: '20', value: 20 },
92+
{ label: '30', value: 30 },
93+
]}
94+
/>
95+
</InstantSearchHooksTestWrapper>
96+
);
97+
98+
await wait(0);
99+
100+
expect(
101+
(getByRole('option', { name: '10' }) as HTMLOptionElement).selected
102+
).toBe(false);
103+
expect(
104+
(getByRole('option', { name: '20' }) as HTMLOptionElement).selected
105+
).toBe(true);
106+
expect(
107+
(getByRole('option', { name: '30' }) as HTMLOptionElement).selected
108+
).toBe(false);
109+
});
110+
111+
test('refines on select', async () => {
112+
const searchClient = createSearchClient();
113+
const { getByRole } = render(
114+
<InstantSearchHooksTestWrapper searchClient={searchClient}>
115+
<HitsPerPage
116+
items={[
117+
{ label: '10', value: 10, default: true },
118+
{ label: '20', value: 20 },
119+
{ label: '30', value: 30 },
120+
]}
121+
/>
122+
</InstantSearchHooksTestWrapper>
123+
);
124+
125+
await wait(0);
126+
127+
expect(searchClient.search).toHaveBeenCalledTimes(1);
128+
129+
userEvent.selectOptions(getByRole('combobox'), ['30']);
130+
131+
expect(searchClient.search).toHaveBeenCalledTimes(2);
132+
expect(searchClient.search).toHaveBeenLastCalledWith(
133+
expect.arrayContaining([
134+
expect.objectContaining({
135+
params: expect.objectContaining({
136+
hitsPerPage: 30,
137+
}),
138+
}),
139+
])
140+
);
141+
});
142+
});

0 commit comments

Comments
 (0)