Create a page limit component

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2022-10-06 09:00:59 +02:00
parent 32aa6532bf
commit 3de4bd4f19
8 changed files with 467 additions and 0 deletions
+26
View File
@@ -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;
@@ -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<{}>) => (
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
<SearchContextProvider>
<Grid container direction="row">
<Grid item xs={12}>
<Story />
</Grid>
</Grid>
</SearchContextProvider>
</TestApiProvider>
),
],
};
export const Default = () => {
return <SearchResultLimiter />;
};
export const CustomLabel = () => {
return <SearchResultLimiter label="Results limit:" />;
};
export const CustomOptions = () => {
return <SearchResultLimiter options={[5, 10, 20]} />;
};
@@ -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(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchResultLimiter />
</SearchContextProvider>
</TestApiProvider>,
);
expect(screen.getByText('Results per page:')).toBeInTheDocument();
expect(screen.getByText('25')).toBeInTheDocument();
});
it('Define default options', async () => {
await renderWithEffects(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchResultLimiter />
</SearchContextProvider>
</TestApiProvider>,
);
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(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchResultLimiter />
</SearchContextProvider>
</TestApiProvider>,
);
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(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchResultLimiter label={label} />
</SearchContextProvider>
</TestApiProvider>,
);
expect(screen.getByText(label)).toBeInTheDocument();
});
it('Accept custom options', async () => {
await renderWithEffects(
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<SearchContextProvider>
<SearchResultLimiter options={[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');
});
});
@@ -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 (
<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();
const handleChange = useCallback(
(newPageLimit: number) => {
setPageLimit(newPageLimit);
},
[setPageLimit],
);
return (
<SearchResultLimiterBase
{...props}
value={pageLimit}
onChange={handleChange}
/>
);
};
@@ -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';
@@ -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';