Merge pull request #18236 from backstage/fix-advance-search-qeury-redirect

[Search] Fix SearchModal and HomePageSearchBar debounce
This commit is contained in:
Ben Lambert
2023-06-16 12:09:43 +02:00
committed by GitHub
6 changed files with 153 additions and 41 deletions
@@ -0,0 +1,59 @@
/*
* Copyright 2023 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 { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
import { searchApiRef } from '@backstage/plugin-search-react';
import { rootRouteRef } from '../../plugin';
import { HomePageSearchBar } from './HomePageSearchBar';
const navigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => navigate,
}));
describe('<HomePageSearchBar/>', () => {
const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) };
it("Don't wait query debounce time when enter is pressed", async () => {
await renderInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<HomePageSearchBar />
</TestApiProvider>,
{
mountedRoutes: {
'/search': rootRouteRef,
},
},
);
expect(searchApiMock.query).toHaveBeenCalledWith(
expect.objectContaining({ term: '' }),
);
await userEvent.type(screen.getByLabelText('Search'), 'term{enter}');
expect(navigate).toHaveBeenCalledWith('/search?query=term');
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useCallback, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import {
SearchBarBase,
@@ -47,11 +47,15 @@ export type HomePageSearchBarProps = Partial<
export const HomePageSearchBar = (props: HomePageSearchBarProps) => {
const classes = useStyles(props);
const [query, setQuery] = useState('');
const ref = useRef<HTMLInputElement | null>(null);
const handleSearch = useNavigateToQuery();
const handleSubmit = () => {
handleSearch({ query });
};
// This handler is called when "enter" is pressed
const handleSubmit = useCallback(() => {
// Using ref to get the current field value without waiting for a query debounce
handleSearch({ query: ref.current?.value ?? '' });
}, [handleSearch]);
const handleChange = useCallback(
value => {
@@ -65,6 +69,7 @@ export const HomePageSearchBar = (props: HomePageSearchBarProps) => {
value={query}
onSubmit={handleSubmit}
onChange={handleChange}
inputProps={{ ref }}
InputProps={{
...props.InputProps,
classes: {
@@ -28,6 +28,13 @@ import {
import { SearchModal } from './SearchModal';
const navigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => navigate,
}));
describe('SearchModal', () => {
const query = jest.fn().mockResolvedValue({ results: [] });
@@ -168,4 +175,36 @@ describe('SearchModal', () => {
expect(screen.getByLabelText('Search')).toHaveFocus();
});
it("Don't wait query debounce time when enter is pressed", async () => {
const initialState = {
term: 'term',
filters: {},
types: [],
pageCursor: '',
};
await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<SearchContextProvider initialState={initialState}>
<SearchModal open hidden={false} toggleModal={toggleModal} />
</SearchContextProvider>
</ApiProvider>,
{
mountedRoutes: {
'/search': rootRouteRef,
},
},
);
expect(query).toHaveBeenCalledWith(
expect.objectContaining({ term: 'term' }),
);
const input = screen.getByLabelText('Search');
await userEvent.clear(input);
await userEvent.type(input, 'new term{enter}');
expect(navigate).toHaveBeenCalledWith('/search?query=new term');
});
});
@@ -13,14 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Link, useContent } from '@backstage/core-components';
import { useContent } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import {
SearchBar,
SearchContextProvider,
SearchResult,
SearchResultPager,
useSearch,
} from '@backstage/plugin-search-react';
import {
Dialog,
@@ -37,7 +36,7 @@ import IconButton from '@material-ui/core/IconButton';
import { makeStyles } from '@material-ui/core/styles';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import CloseIcon from '@material-ui/icons/Close';
import React, { KeyboardEvent, useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { rootRouteRef } from '../../plugin';
@@ -92,6 +91,11 @@ const useStyles = makeStyles(theme => ({
input: {
flex: 1,
},
button: {
'&:hover': {
background: 'none',
},
},
// Reduces default height of the modal, keeping a gap of 128px between the top and bottom of the page.
paperFullWidth: { height: 'calc(100% - 128px)' },
dialogActionsContainer: { padding: theme.spacing(1, 3) },
@@ -104,9 +108,8 @@ export const Modal = ({ toggleModal }: SearchModalChildrenProps) => {
const { transitions } = useTheme();
const { focusContent } = useContent();
const { term } = useSearch();
const searchRootRoute = useRouteRef(rootRouteRef)();
const searchBarRef = useRef<HTMLInputElement | null>(null);
const searchPagePath = `${useRouteRef(rootRouteRef)()}?query=${term}`;
useEffect(() => {
searchBarRef?.current?.focus();
@@ -116,15 +119,13 @@ export const Modal = ({ toggleModal }: SearchModalChildrenProps) => {
setTimeout(focusContent, transitions.duration.leavingScreen);
}, [focusContent, transitions]);
const handleSearchBarKeyDown = useCallback(
(e: KeyboardEvent<HTMLDivElement | HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
navigate(searchPagePath);
handleSearchResultClick();
}
},
[navigate, handleSearchResultClick, searchPagePath],
);
// This handler is called when "enter" is pressed
const handleSearchBarSubmit = useCallback(() => {
// Using ref to get the current field value without waiting for a query debounce
const query = searchBarRef.current?.value ?? '';
navigate(`${searchRootRoute}?query=${query}`);
handleSearchResultClick();
}, [navigate, handleSearchResultClick, searchRootRoute]);
return (
<>
@@ -133,7 +134,7 @@ export const Modal = ({ toggleModal }: SearchModalChildrenProps) => {
<SearchBar
className={classes.input}
inputProps={{ ref: searchBarRef }}
onKeyDown={handleSearchBarKeyDown}
onSubmit={handleSearchBarSubmit}
/>
<IconButton aria-label="close" onClick={toggleModal}>
@@ -150,11 +151,11 @@ export const Modal = ({ toggleModal }: SearchModalChildrenProps) => {
>
<Grid item>
<Button
to={searchPagePath}
onClick={handleSearchResultClick}
endIcon={<ArrowForwardIcon />}
component={Link}
className={classes.button}
color="primary"
endIcon={<ArrowForwardIcon />}
onClick={handleSearchResultClick}
disableRipple
>
View Full Results
</Button>