diff --git a/.changeset/search-days-pull.md b/.changeset/search-days-pull.md
new file mode 100644
index 0000000000..72fb9974a4
--- /dev/null
+++ b/.changeset/search-days-pull.md
@@ -0,0 +1,98 @@
+---
+'@backstage/plugin-search-react': minor
+---
+
+A `` 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.
+
+See examples below:
+
+_Basic_
+
+```jsx
+import React, { useState } from 'react';
+import { Grid } from '@material-ui/core';
+import { Page, Header, Content, Lifecycle } from '@backstage/core-components';
+import {
+ SearchBarBase,
+ SearchResultLimiterBase,
+ SearchResultList,
+} from '@backstage/plugin-search-react';
+
+const SearchPage = () => {
+ const [term, setTerm] = useState('');
+ const [pageLimit, setPageLimit] = useState(25);
+
+ return (
+
+ } />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+```
+
+_With context_
+
+```jsx
+import React from 'react';
+import { Grid } from '@material-ui/core';
+import { Page, Header, Content, Lifecycle } from '@backstage/core-components';
+import {
+ SearchBar,
+ SearchResult,
+ SearchResultLimiter,
+ SearchResultListLayout,
+ SearchContextProvider,
+ DefaultResultListItem,
+} from '@backstage/plugin-search-react';
+
+const SearchPage = () => (
+
+
+ } />
+
+
+
+
+
+
+
+
+
+
+ {({ results }) => (
+ (
+
+ )}
+ />
+ )}
+
+
+
+
+
+
+);
+```
diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx
index 99814130c2..b97ac99558 100644
--- a/packages/app/src/components/search/SearchPage.tsx
+++ b/packages/app/src/components/search/SearchPage.tsx
@@ -35,6 +35,7 @@ import {
SearchBar,
SearchFilter,
SearchResult,
+ SearchResultLimiter,
SearchResultPager,
useSearch,
} from '@backstage/plugin-search-react';
@@ -55,6 +56,10 @@ const useStyles = makeStyles((theme: Theme) => ({
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
+ limiter: {
+ width: '100%',
+ justifyContent: 'flex-end',
+ },
}));
const SearchPage = () => {
@@ -129,6 +134,7 @@ const SearchPage = () => {
)}
+
{({ results }) => (
diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md
index ddfe5d5b39..744f45065f 100644
--- a/plugins/search-react/api-report.md
+++ b/plugins/search-react/api-report.md
@@ -320,6 +320,32 @@ 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?: number[];
+ value?: number;
+ onChange?: (value: number) => void;
+};
+
+// @public
+export type SearchResultLimiterProps = Omit<
+ SearchResultLimiterBaseProps,
+ 'value' | 'onChange'
+>;
+
// @public
export const SearchResultList: (props: SearchResultListProps) => JSX.Element;
diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx
new file mode 100644
index 0000000000..f098f1ab68
--- /dev/null
+++ b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.stories.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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, { ComponentType } from 'react';
+import { Grid } from '@material-ui/core';
+
+import { TestApiProvider } from '@backstage/test-utils';
+
+import { searchApiRef, MockSearchApi } from '../../api';
+import { SearchContextProvider } from '../../context';
+
+import { SearchResultLimiter } from './SearchResultLimiter';
+
+export default {
+ title: 'Plugins/Search/SearchResultLimiter',
+ component: SearchResultLimiter,
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+
+
+
+
+ ),
+ ],
+};
+
+export const Default = () => {
+ return ;
+};
+
+export const CustomLabel = () => {
+ return ;
+};
+
+export const CustomOptions = () => {
+ return ;
+};
diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx
new file mode 100644
index 0000000000..a6f9f5b1e9
--- /dev/null
+++ b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.test.tsx
@@ -0,0 +1,118 @@
+/*
+ * 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 from 'react';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
+
+import { searchApiRef } from '../../api';
+import { SearchContextProvider } from '../../context';
+
+import { SearchResultLimiter } from './SearchResultLimiter';
+
+const query = jest.fn().mockResolvedValue({ results: [] });
+
+describe('SearchResultLimiter', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('Renders without exploding', async () => {
+ await renderWithEffects(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByText('Results per page:')).toBeInTheDocument();
+ expect(screen.getByText('25')).toBeInTheDocument();
+ });
+
+ it('Define default options', async () => {
+ await renderWithEffects(
+
+
+
+
+ ,
+ );
+
+ await userEvent.click(screen.getByText('25'));
+
+ const options = screen.getAllByRole('option');
+ expect(options).toHaveLength(4);
+ expect(options[0]).toHaveTextContent('10');
+ expect(options[1]).toHaveTextContent('25');
+ expect(options[2]).toHaveTextContent('50');
+ expect(options[3]).toHaveTextContent('100');
+ });
+
+ it('Set page limit in the context', async () => {
+ await renderWithEffects(
+
+
+
+
+ ,
+ );
+
+ await userEvent.click(screen.getByText('25'));
+
+ await userEvent.click(screen.getByText('10'));
+
+ expect(query).toHaveBeenCalledWith(
+ expect.objectContaining({
+ pageLimit: 10,
+ }),
+ );
+ });
+
+ it('Accept custom label', async () => {
+ const label = 'Custom label';
+ await renderWithEffects(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByText(label)).toBeInTheDocument();
+ });
+
+ it('Accept custom options', async () => {
+ await renderWithEffects(
+
+
+
+
+ ,
+ );
+
+ 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');
+ });
+});
diff --git a/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx
new file mode 100644
index 0000000000..0d4b8381dd
--- /dev/null
+++ b/plugins/search-react/src/components/SearchResultLimiter/SearchResultLimiter.tsx
@@ -0,0 +1,146 @@
+/*
+ * 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';
+
+/**
+ * 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?: number[];
+ /**
+ * 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 (
+
+
+ {label}
+
+ }
+ value={value}
+ onChange={handleChange}
+ >
+ {[...new Set([DEFAULT_PAGE_LIMIT, ...options])]
+ .sort((a, b) => a - b)
+ .map(option => (
+
+ ))}
+
+
+ );
+};
+
+/**
+ * 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();
+
+ const handleChange = useCallback(
+ (newPageLimit: number) => {
+ setPageLimit(newPageLimit);
+ },
+ [setPageLimit],
+ );
+
+ return (
+
+ );
+};
diff --git a/plugins/search-react/src/components/SearchResultLimiter/index.ts b/plugins/search-react/src/components/SearchResultLimiter/index.ts
new file mode 100644
index 0000000000..411e48b5f5
--- /dev/null
+++ b/plugins/search-react/src/components/SearchResultLimiter/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+export * from './SearchResultLimiter';
diff --git a/plugins/search-react/src/components/index.ts b/plugins/search-react/src/components/index.ts
index 62d8b611a4..524b0f9194 100644
--- a/plugins/search-react/src/components/index.ts
+++ b/plugins/search-react/src/components/index.ts
@@ -20,6 +20,7 @@ export * from './SearchAutocomplete';
export * from './SearchFilter';
export * from './SearchResult';
export * from './SearchResultPager';
+export * from './SearchResultLimiter';
export * from './SearchResultList';
export * from './SearchResultGroup';
export * from './DefaultResultListItem';