refactor(search-react): use mui table pagination component
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -2,9 +2,7 @@
|
||||
'@backstage/plugin-search-react': minor
|
||||
---
|
||||
|
||||
A `<SearchResultLimiter />` component was created for limiting the number of results shown per search page.
|
||||
Use this new component to give users a combination of options to define how many search results they want to display per page.
|
||||
The default options are 10, 25, 50, 100.
|
||||
A `<SearchPagination />` component was created for limiting the number of results shown per search page. Use this new component to give users options to select how many search results they want to display per page. The default options are 10, 25, 50, 100.
|
||||
|
||||
See examples below:
|
||||
|
||||
@@ -16,13 +14,14 @@ import { Grid } from '@material-ui/core';
|
||||
import { Page, Header, Content, Lifecycle } from '@backstage/core-components';
|
||||
import {
|
||||
SearchBarBase,
|
||||
SearchResultLimiterBase,
|
||||
SearchPaginationBase,
|
||||
SearchResultList,
|
||||
} from '@backstage/plugin-search-react';
|
||||
|
||||
const SearchPage = () => {
|
||||
const [term, setTerm] = useState('');
|
||||
const [pageLimit, setPageLimit] = useState(25);
|
||||
const [pageCursor, setPageCursor] = useState<string>();
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
@@ -33,9 +32,11 @@ const SearchPage = () => {
|
||||
<SearchBarBase value={term} onChange={setTerm} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResultLimiterBase
|
||||
value={pageLimit}
|
||||
onChange={setPageLimit}
|
||||
<SearchPaginationBase
|
||||
pageLimit={pageLimit}
|
||||
onPageLimitChange={setPageLimit}
|
||||
pageCursor={pageCursor}
|
||||
onPageCursorChange={setPageCursor}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
@@ -57,7 +58,7 @@ import { Page, Header, Content, Lifecycle } from '@backstage/core-components';
|
||||
import {
|
||||
SearchBar,
|
||||
SearchResult,
|
||||
SearchResultLimiter,
|
||||
SearchPagination,
|
||||
SearchResultListLayout,
|
||||
SearchContextProvider,
|
||||
DefaultResultListItem,
|
||||
@@ -73,7 +74,7 @@ const SearchPage = () => (
|
||||
<SearchBar />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResultLimiter />
|
||||
<SearchPagination />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResult>
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
SearchBar,
|
||||
SearchFilter,
|
||||
SearchResult,
|
||||
SearchResultLimiter,
|
||||
SearchPagination,
|
||||
SearchResultPager,
|
||||
useSearch,
|
||||
} from '@backstage/plugin-search-react';
|
||||
@@ -56,10 +56,6 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||
padding: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
limiter: {
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
}));
|
||||
|
||||
const SearchPage = () => {
|
||||
@@ -134,7 +130,7 @@ const SearchPage = () => {
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs>
|
||||
<SearchResultLimiter className={classes.limiter} />
|
||||
<SearchPagination />
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
<List>
|
||||
|
||||
@@ -211,6 +211,50 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & {
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const SearchPagination: (props: SearchPaginationProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const SearchPaginationBase: (
|
||||
props: SearchPaginationBaseProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type SearchPaginationBaseProps = {
|
||||
className?: string;
|
||||
pageCursor?: string;
|
||||
onPageCursorChange?: (pageCursor: string) => void;
|
||||
pageLimit?: number;
|
||||
pageLimitLabel?: ReactNode;
|
||||
pageLimitText?: SearchPaginationLimitText;
|
||||
pageLimitOptions?: SearchPaginationLimitOption[];
|
||||
onPageLimitChange?: (value: number) => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type SearchPaginationLimitOption<
|
||||
Current extends number = 101,
|
||||
Accumulator extends number[] = [],
|
||||
> = Accumulator['length'] extends Current
|
||||
? Accumulator[number]
|
||||
: SearchPaginationLimitOption<
|
||||
Current,
|
||||
[...Accumulator, Accumulator['length']]
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type SearchPaginationLimitText = (params: {
|
||||
from: number;
|
||||
to: number;
|
||||
page: number;
|
||||
}) => ReactNode;
|
||||
|
||||
// @public
|
||||
export type SearchPaginationProps = Omit<
|
||||
SearchPaginationBaseProps,
|
||||
'pageLimit' | 'onPageLimitChange' | 'pageCursor' | 'onPageCursorChange'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const SearchResult: (props: SearchResultProps) => JSX.Element;
|
||||
|
||||
@@ -320,40 +364,6 @@ export const SearchResultGroupTextFilterField: (
|
||||
export type SearchResultGroupTextFilterFieldProps =
|
||||
SearchResultGroupFilterFieldPropsWith<{}>;
|
||||
|
||||
// @public
|
||||
export const SearchResultLimiter: (
|
||||
props: SearchResultLimiterProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const SearchResultLimiterBase: (
|
||||
props: SearchResultLimiterBaseProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type SearchResultLimiterBaseProps = {
|
||||
id?: string;
|
||||
className?: string;
|
||||
label?: ReactNode;
|
||||
options?: SearchResultLimiterOption[];
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type SearchResultLimiterOption<
|
||||
Current extends number = 101,
|
||||
Accumulator extends number[] = [],
|
||||
> = Accumulator['length'] extends Current
|
||||
? Accumulator[number]
|
||||
: SearchResultLimiterOption<Current, [...Accumulator, Accumulator['length']]>;
|
||||
|
||||
// @public
|
||||
export type SearchResultLimiterProps = Omit<
|
||||
SearchResultLimiterBaseProps,
|
||||
'value' | 'onChange'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const SearchResultList: (props: SearchResultListProps) => JSX.Element;
|
||||
|
||||
|
||||
+12
-8
@@ -22,11 +22,11 @@ import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
|
||||
import { SearchResultLimiter } from './SearchResultLimiter';
|
||||
import { SearchPagination } from './SearchPagination';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Search/SearchResultLimiter',
|
||||
component: SearchResultLimiter,
|
||||
title: 'Plugins/Search/SearchPagination',
|
||||
component: SearchPagination,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
@@ -43,13 +43,17 @@ export default {
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return <SearchResultLimiter />;
|
||||
return <SearchPagination />;
|
||||
};
|
||||
|
||||
export const CustomLabel = () => {
|
||||
return <SearchResultLimiter label="Results limit:" />;
|
||||
export const CustomPageLimitLabel = () => {
|
||||
return <SearchPagination pageLimitLabel="Page limit:" />;
|
||||
};
|
||||
|
||||
export const CustomOptions = () => {
|
||||
return <SearchResultLimiter options={[5, 10, 20]} />;
|
||||
export const CustomPageLimitText = () => {
|
||||
return <SearchPagination pageLimitText={({ from, to }) => `${from}-${to}`} />;
|
||||
};
|
||||
|
||||
export const CustomPageLimitOptions = () => {
|
||||
return <SearchPagination pageLimitOptions={[5, 10, 20]} />;
|
||||
};
|
||||
+83
-27
@@ -23,11 +23,15 @@ import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
|
||||
import { searchApiRef } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
|
||||
import { SearchResultLimiter } from './SearchResultLimiter';
|
||||
import { SearchPagination } from './SearchPagination';
|
||||
|
||||
const query = jest.fn().mockResolvedValue({ results: [] });
|
||||
const query = jest.fn().mockResolvedValue({
|
||||
results: [],
|
||||
nextPageCursor: 'Mg==',
|
||||
previousPageCursor: 'MA==',
|
||||
});
|
||||
|
||||
describe('SearchResultLimiter', () => {
|
||||
describe('SearchPagination', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -36,20 +40,23 @@ describe('SearchResultLimiter', () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter />
|
||||
<SearchPagination />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Results per page:')).toBeInTheDocument();
|
||||
expect(screen.getByText('25')).toBeInTheDocument();
|
||||
expect(screen.getByText('1-25 of more than 25')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Next page')).toBeEnabled();
|
||||
expect(screen.getByLabelText('Previous page')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('Define default options', async () => {
|
||||
it('Define default page limit options', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter />
|
||||
<SearchPagination />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
@@ -64,11 +71,55 @@ describe('SearchResultLimiter', () => {
|
||||
expect(options[3]).toHaveTextContent('100');
|
||||
});
|
||||
|
||||
it('Accept custom page limit label', async () => {
|
||||
const label = 'Page limit:';
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchPagination pageLimitLabel={label} />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Accept custom page limit text', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchPagination pageLimitText={({ from, to }) => `${from}-${to}`} />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('1-25')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Accept custom page limit options', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchPagination pageLimitOptions={[5, 10, 20, 25]} />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('25'));
|
||||
|
||||
const options = screen.getAllByRole('option');
|
||||
expect(options).toHaveLength(4);
|
||||
expect(options[0]).toHaveTextContent('5');
|
||||
expect(options[1]).toHaveTextContent('10');
|
||||
expect(options[2]).toHaveTextContent('20');
|
||||
expect(options[3]).toHaveTextContent('25');
|
||||
});
|
||||
|
||||
it('Set page limit in the context', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter />
|
||||
<SearchPagination />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
@@ -84,35 +135,40 @@ describe('SearchResultLimiter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Accept custom label', async () => {
|
||||
const label = 'Custom label';
|
||||
it('Set page cursor in the context', async () => {
|
||||
const initialState = {
|
||||
term: '',
|
||||
types: [],
|
||||
filters: {},
|
||||
pageCursor: 'MQ==', // page: 1
|
||||
};
|
||||
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter label={label} />
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<SearchPagination />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(label)).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(screen.getByLabelText('Next page'));
|
||||
|
||||
it('Accept custom options', async () => {
|
||||
await renderWithEffects(
|
||||
<TestApiProvider apis={[[searchApiRef, { query }]]}>
|
||||
<SearchContextProvider>
|
||||
<SearchResultLimiter options={[5, 10, 20, 25]} />
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>,
|
||||
expect(screen.getByText('51-75 of more than 75')).toBeInTheDocument();
|
||||
|
||||
expect(query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
pageCursor: 'Mg==', // page: 2
|
||||
}),
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('25'));
|
||||
await userEvent.click(screen.getByLabelText('Previous page'));
|
||||
|
||||
const options = screen.getAllByRole('option');
|
||||
expect(options).toHaveLength(4);
|
||||
expect(options[0]).toHaveTextContent('5');
|
||||
expect(options[1]).toHaveTextContent('10');
|
||||
expect(options[2]).toHaveTextContent('20');
|
||||
expect(options[3]).toHaveTextContent('25');
|
||||
expect(screen.getByText('26-50 of more than 50')).toBeInTheDocument();
|
||||
|
||||
expect(query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
pageCursor: 'MQ==', // page: 1
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
ReactNode,
|
||||
ChangeEvent,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { TablePagination } from '@material-ui/core';
|
||||
import { useSearch } from '../../context';
|
||||
|
||||
const encodePageCursor = (pageCursor: number): string => {
|
||||
return Buffer.from(pageCursor.toString(), 'utf-8').toString('base64');
|
||||
};
|
||||
|
||||
const decodePageCursor = (pageCursor?: string): number => {
|
||||
if (!pageCursor) return 0;
|
||||
return Number(Buffer.from(pageCursor, 'base64').toString('utf-8'));
|
||||
};
|
||||
|
||||
/**
|
||||
* A page limit option, this value must not be greater than 100.
|
||||
* @public
|
||||
*/
|
||||
export type SearchPaginationLimitOption<
|
||||
Current extends number = 101,
|
||||
Accumulator extends number[] = [],
|
||||
> = Accumulator['length'] extends Current
|
||||
? Accumulator[number]
|
||||
: SearchPaginationLimitOption<
|
||||
Current,
|
||||
[...Accumulator, Accumulator['length']]
|
||||
>;
|
||||
|
||||
/**
|
||||
* A page limit text, this function is called with a "\{ from, to, page \}" object.
|
||||
* @public
|
||||
*/
|
||||
export type SearchPaginationLimitText = (params: {
|
||||
from: number;
|
||||
to: number;
|
||||
page: number;
|
||||
}) => ReactNode;
|
||||
|
||||
/**
|
||||
* Props for {@link SearchPaginationBase}.
|
||||
* @public
|
||||
*/
|
||||
export type SearchPaginationBaseProps = {
|
||||
/**
|
||||
* The component class name.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The cursor for the current page.
|
||||
*/
|
||||
pageCursor?: string;
|
||||
/**
|
||||
* Callback fired when the current page cursor is changed.
|
||||
*/
|
||||
onPageCursorChange?: (pageCursor: string) => void;
|
||||
/**
|
||||
* The limit of results per page.
|
||||
* Set -1 to display all the results.
|
||||
*/
|
||||
pageLimit?: number;
|
||||
/**
|
||||
* Customize the results per page label.
|
||||
*/
|
||||
pageLimitLabel?: ReactNode;
|
||||
/**
|
||||
* Customize the results per page text.
|
||||
*/
|
||||
pageLimitText?: SearchPaginationLimitText;
|
||||
/**
|
||||
* Options for setting how many results show per page.
|
||||
* If less than two options are available, no select field will be displayed.
|
||||
* Use -1 for the value with a custom label to show all the results.
|
||||
*/
|
||||
pageLimitOptions?: SearchPaginationLimitOption[];
|
||||
/**
|
||||
* Callback fired when the number of results per page is changed.
|
||||
*/
|
||||
onPageLimitChange?: (value: number) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A component with controls for search results pagination.
|
||||
* @param props - See {@link SearchPaginationBaseProps}.
|
||||
* @public
|
||||
*/
|
||||
export const SearchPaginationBase = (props: SearchPaginationBaseProps) => {
|
||||
const {
|
||||
pageCursor,
|
||||
onPageCursorChange,
|
||||
pageLimit: rowsPerPage = 25,
|
||||
pageLimitLabel: labelRowsPerPage = 'Results per page:',
|
||||
pageLimitText: labelDisplayedRows,
|
||||
pageLimitOptions: rowsPerPageOptions,
|
||||
onPageLimitChange,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const page = useMemo(() => decodePageCursor(pageCursor), [pageCursor]);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(_: MouseEvent<HTMLButtonElement> | null, newValue: number) => {
|
||||
onPageCursorChange?.(encodePageCursor(newValue));
|
||||
},
|
||||
[onPageCursorChange],
|
||||
);
|
||||
|
||||
const handleRowsPerPageChange = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
onPageLimitChange?.(parseInt(newValue, 10));
|
||||
},
|
||||
[onPageLimitChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<TablePagination
|
||||
{...rest}
|
||||
component="div"
|
||||
count={-1}
|
||||
page={page}
|
||||
onPageChange={handlePageChange}
|
||||
rowsPerPage={rowsPerPage}
|
||||
labelRowsPerPage={labelRowsPerPage}
|
||||
labelDisplayedRows={labelDisplayedRows}
|
||||
rowsPerPageOptions={rowsPerPageOptions}
|
||||
onRowsPerPageChange={handleRowsPerPageChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchPagination}.
|
||||
* @public
|
||||
*/
|
||||
export type SearchPaginationProps = Omit<
|
||||
SearchPaginationBaseProps,
|
||||
'pageLimit' | 'onPageLimitChange' | 'pageCursor' | 'onPageCursorChange'
|
||||
>;
|
||||
|
||||
/**
|
||||
* A component for setting the search context page limit and cursor.
|
||||
* @param props - See {@link SearchPaginationProps}.
|
||||
* @public
|
||||
*/
|
||||
export const SearchPagination = (props: SearchPaginationProps) => {
|
||||
const { pageLimit, setPageLimit, pageCursor, setPageCursor } = useSearch();
|
||||
|
||||
return (
|
||||
<SearchPaginationBase
|
||||
{...props}
|
||||
pageLimit={pageLimit}
|
||||
onPageLimitChange={setPageLimit}
|
||||
pageCursor={pageCursor}
|
||||
onPageCursorChange={setPageCursor}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './SearchResultLimiter';
|
||||
export * from './SearchPagination';
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, ChangeEvent, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
InputBase,
|
||||
MenuItem,
|
||||
Select,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@material-ui/core';
|
||||
import { useSearch } from '../../context';
|
||||
|
||||
/**
|
||||
* A page limit option, this value must not be greater than 100.
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultLimiterOption<
|
||||
Current extends number = 101,
|
||||
Accumulator extends number[] = [],
|
||||
> = Accumulator['length'] extends Current
|
||||
? Accumulator[number]
|
||||
: SearchResultLimiterOption<Current, [...Accumulator, Accumulator['length']]>;
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultLimiterBase}.
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultLimiterBaseProps = {
|
||||
id?: string;
|
||||
className?: string;
|
||||
/**
|
||||
* A label for the combobox.
|
||||
*/
|
||||
label?: ReactNode;
|
||||
/**
|
||||
* The combobox labels, defaults to 10, 25, 50 and 100.
|
||||
*/
|
||||
options?: SearchResultLimiterOption[];
|
||||
/**
|
||||
* Combobox selected option, defaults to 25;
|
||||
*/
|
||||
value?: number;
|
||||
/**
|
||||
* The callback handler called when the selected option changed.
|
||||
*/
|
||||
onChange?: (value: number) => void;
|
||||
};
|
||||
|
||||
const DEFAULT_PAGE_LIMIT = 25;
|
||||
|
||||
/**
|
||||
* A component for selecting the number of results per page.
|
||||
* @param props - See {@link SearchResultLimiterBaseProps}.
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultLimiterBase = (
|
||||
props: SearchResultLimiterBaseProps,
|
||||
) => {
|
||||
const {
|
||||
id = 'search-result-limiter',
|
||||
className,
|
||||
label = 'Results per page:',
|
||||
options = [10, 50, 100],
|
||||
value = DEFAULT_PAGE_LIMIT,
|
||||
onChange = () => {},
|
||||
} = props;
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<{ value: unknown }>) => {
|
||||
const newValue = e.target.value;
|
||||
if (typeof newValue === 'number') {
|
||||
onChange(newValue);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={className}
|
||||
display="inline-grid"
|
||||
gridGap={theme.spacing(0.5)}
|
||||
gridAutoFlow="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography id={`${id}-label`} variant="body2">
|
||||
{label}
|
||||
</Typography>
|
||||
<Select
|
||||
id={`${id}-select`}
|
||||
labelId={`${id}-label`}
|
||||
variant="standard"
|
||||
input={<InputBase />}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{[...new Set([DEFAULT_PAGE_LIMIT, ...options])]
|
||||
.sort((a, b) => a - b)
|
||||
.map(option => (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchResultLimiter}.
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultLimiterProps = Omit<
|
||||
SearchResultLimiterBaseProps,
|
||||
'value' | 'onChange'
|
||||
>;
|
||||
|
||||
/**
|
||||
* A component for setting the search context page limit.
|
||||
* @param props - See {@link SearchResultLimiterProps}.
|
||||
* @public
|
||||
*/
|
||||
export const SearchResultLimiter = (props: SearchResultLimiterProps) => {
|
||||
const { pageLimit, setPageLimit } = useSearch();
|
||||
|
||||
return (
|
||||
<SearchResultLimiterBase
|
||||
{...props}
|
||||
value={pageLimit}
|
||||
onChange={setPageLimit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export * from './SearchAutocomplete';
|
||||
export * from './SearchFilter';
|
||||
export * from './SearchResult';
|
||||
export * from './SearchResultPager';
|
||||
export * from './SearchResultLimiter';
|
||||
export * from './SearchPagination';
|
||||
export * from './SearchResultList';
|
||||
export * from './SearchResultGroup';
|
||||
export * from './DefaultResultListItem';
|
||||
|
||||
Reference in New Issue
Block a user