fix: search bar enter redirect
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user