diff --git a/.changeset/honest-beds-appear.md b/.changeset/honest-beds-appear.md new file mode 100644 index 0000000000..b3db4c7c48 --- /dev/null +++ b/.changeset/honest-beds-appear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Fixes `SearchModal` and `HomePageSearchBar` components to use search bar reference value when "enter" is pressed, avoiding waiting for query state debounce. diff --git a/packages/app/src/components/search/SearchModal.tsx b/packages/app/src/components/search/SearchModal.tsx index e2d46b2e19..6b701d394d 100644 --- a/packages/app/src/components/search/SearchModal.tsx +++ b/packages/app/src/components/search/SearchModal.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CatalogIcon, DocsIcon, Link } from '@backstage/core-components'; +import { CatalogIcon, DocsIcon } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { CATALOG_FILTER_EXISTS, @@ -43,7 +43,7 @@ import IconButton from '@material-ui/core/IconButton'; import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; import BuildIcon from '@material-ui/icons/Build'; 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'; const useStyles = makeStyles(theme => ({ @@ -74,6 +74,11 @@ const useStyles = makeStyles(theme => ({ input: { flex: 1, }, + button: { + '&:hover': { + background: 'none', + }, + }, dialogActionsContainer: { padding: theme.spacing(1, 3) }, viewResultsLink: { verticalAlign: '0.5em' }, })); @@ -85,23 +90,21 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { const navigate = useNavigate(); const catalogApi = useApi(catalogApiRef); - const { term, types } = useSearch(); + const { types } = useSearch(); + const searchRootRoute = useRouteRef(rootRouteRef)(); const searchBarRef = useRef(null); - const searchPagePath = `${useRouteRef(rootRouteRef)()}?query=${term}`; useEffect(() => { searchBarRef?.current?.focus(); }); - const handleSearchBarKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'Enter') { - toggleModal(); - navigate(searchPagePath); - } - }, - [navigate, searchPagePath, toggleModal], - ); + // This handler is called when "enter" is pressed + const handleSearchBarSubmit = useCallback(() => { + toggleModal(); + // Using ref to get the current field value without waiting for a query debounce + const query = searchBarRef.current?.value ?? ''; + navigate(`${searchRootRoute}?query=${query}`); + }, [navigate, toggleModal, searchRootRoute]); return ( <> @@ -110,7 +113,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { @@ -189,11 +192,11 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { > diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.test.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.test.tsx new file mode 100644 index 0000000000..069f1bda07 --- /dev/null +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.test.tsx @@ -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('', () => { + const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; + + it("Don't wait query debounce time when enter is pressed", async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + + expect(searchApiMock.query).toHaveBeenCalledWith( + expect.objectContaining({ term: '' }), + ); + + await userEvent.type(screen.getByLabelText('Search'), 'term{enter}'); + + expect(navigate).toHaveBeenCalledWith('/search?query=term'); + }); +}); diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx index b829bf8ac2..47ee15c7f3 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx @@ -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(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: { diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index d24764f5d4..1ab7952a2b 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -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( + + + + , + { + 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'); + }); }); diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index cfeabf3358..777a285107 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -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(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) => { - 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) => { @@ -150,11 +151,11 @@ export const Modal = ({ toggleModal }: SearchModalChildrenProps) => { >