From 49a696d720b8dde6c0acfd6399e84d8a825e7e88 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 23 Dec 2021 12:05:26 +0100 Subject: [PATCH] [Search] Standardizes input search components (#8532) * refactor(plugin-search): customize search bar components Signed-off-by: Camila Belo * refactor(plugin-search): use search bar in search modal Signed-off-by: Camila Belo * refactor(plugin-search): customize home page search bar Signed-off-by: Camila Belo * chore(plugin-search): update api reports Signed-off-by: Camila Belo * chore: add changeset file Signed-off-by: Camila Belo * apply review suggestions Signed-off-by: Camila Belo * increase default debounceTime and remove prop from SearchBar in SearchModal + SearchPage in app Signed-off-by: Emma Indal * update test using default debounce time Signed-off-by: Emma Indal * add changeset for create-app Signed-off-by: Emma Indal * fix exports after rebase Signed-off-by: Emma Indal Co-authored-by: Emma Indal --- .changeset/bright-dancers-tickle.md | 5 + .changeset/search-seahorses-tie.md | 15 ++ .../app/src/components/search/SearchPage.tsx | 2 +- plugins/search/api-report.md | 75 ++++--- .../HomePageComponent/HomePageSearchBar.tsx | 41 ++-- .../src/components/HomePageComponent/index.ts | 1 + .../components/SearchBar/SearchBar.test.tsx | 7 + .../src/components/SearchBar/SearchBar.tsx | 185 ++++++++++-------- .../search/src/components/SearchBar/index.tsx | 1 + .../components/SearchModal/SearchModal.tsx | 34 +--- plugins/search/src/components/index.tsx | 1 + plugins/search/src/index.ts | 6 +- 12 files changed, 215 insertions(+), 158 deletions(-) create mode 100644 .changeset/bright-dancers-tickle.md create mode 100644 .changeset/search-seahorses-tie.md diff --git a/.changeset/bright-dancers-tickle.md b/.changeset/bright-dancers-tickle.md new file mode 100644 index 0000000000..aabe94056f --- /dev/null +++ b/.changeset/bright-dancers-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +debounceTime prop is removed from the SearchBar component in the SearchPage as the default is set to 200. The prop is safe to remove and makes it easier to stay up to date with any changes in the future. diff --git a/.changeset/search-seahorses-tie.md b/.changeset/search-seahorses-tie.md new file mode 100644 index 0000000000..f803b94b4d --- /dev/null +++ b/.changeset/search-seahorses-tie.md @@ -0,0 +1,15 @@ +--- +'@backstage/plugin-search': patch +--- + +Standardizes the component used as a search box in the search modal and in the composable home page. + +After these changes, all search boxes exported by the search plugin are based on the `` component, and this one is based on the `` component of the Material UI. This means that when you use SearchBarBase or one of its derived components (like `SearchBar` and `HomePageSearchBar`) you can pass all properties accepted by InputBase that have not been replaced by the props type of those components. + +For example: + +```jsx + +``` + +The `color` property is inherited from `InputBaseProps` type and `debouceTime` defined by `SearchBarBaseProps`. diff --git a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx index 50ffbadb76..95c8c64c62 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx @@ -37,7 +37,7 @@ const SearchPage = () => { - + diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 13a9b15ea1..8c8594ca1d 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -10,6 +10,7 @@ import { AsyncState } from 'react-use/lib/useAsync'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { IndexableDocument } from '@backstage/search-common'; +import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; @@ -65,10 +66,14 @@ export type FiltersState = { // // @public (undocumented) export const HomePageSearchBar: ({ - placeholder, -}: { - placeholder?: string | undefined; -}) => JSX.Element; + className: defaultClassName, + ...props +}: Partial>) => JSX.Element; + +// @public +export type HomePageSearchBarProps = Partial< + Omit +>; // Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -88,34 +93,42 @@ export interface SearchApi { // @public (undocumented) export const searchApiRef: ApiRef; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const SearchBar: ({ - autoFocus, - className, +// @public +export const SearchBar: ({ onChange, ...props }: SearchBarProps) => JSX.Element; + +// @public +export const SearchBarBase: ({ + onChange, + onKeyDown, + onSubmit, debounceTime, - placeholder, clearButton, -}: Props) => JSX.Element; + fullWidth, + value: defaultValue, + inputProps: defaultInputProps, + endAdornment: defaultEndAdornment, + ...props +}: SearchBarBaseProps) => JSX.Element; + +// @public +export type SearchBarBaseProps = Omit & { + debounceTime?: number; + clearButton?: boolean; + onClear?: () => void; + onSubmit?: () => void; + onChange: (value: string) => void; +}; // Warning: (ae-missing-release-tag) "SearchBarNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) export const SearchBarNext: ({ - autoFocus, - className, - debounceTime, - placeholder, - clearButton, -}: { - autoFocus?: boolean | undefined; - className?: string | undefined; - debounceTime?: number | undefined; - placeholder?: string | undefined; - clearButton?: boolean | undefined; -}) => JSX.Element; + onChange, + ...props +}: Partial) => JSX.Element; + +// @public +export type SearchBarProps = Partial; // Warning: (ae-missing-release-tag) "SearchContextProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -131,18 +144,18 @@ export const SearchContextProvider: ({ // // @public (undocumented) export const SearchFilter: { - ({ component: Element, ...props }: Props_2): JSX.Element; - Checkbox(props: Omit & Component): JSX.Element; - Select(props: Omit & Component): JSX.Element; + ({ component: Element, ...props }: Props): JSX.Element; + Checkbox(props: Omit & Component): JSX.Element; + Select(props: Omit & Component): JSX.Element; }; // Warning: (ae-missing-release-tag) "SearchFilterNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) export const SearchFilterNext: { - ({ component: Element, ...props }: Props_2): JSX.Element; - Checkbox(props: Omit & Component): JSX.Element; - Select(props: Omit & Component): JSX.Element; + ({ component: Element, ...props }: Props): JSX.Element; + Checkbox(props: Omit & Component): JSX.Element; + Select(props: Omit & Component): JSX.Element; }; // Warning: (ae-missing-release-tag) "SearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx index 94c72df3a8..20517be978 100644 --- a/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx +++ b/plugins/search/src/components/HomePageComponent/HomePageSearchBar.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import { SearchBarBase } from '../SearchBar'; +import { SearchBarBase, SearchBarBaseProps } from '../SearchBar'; import { useNavigateToQuery } from '../util'; const useStyles = makeStyles({ @@ -28,20 +28,37 @@ const useStyles = makeStyles({ }, }); -type Props = { - placeholder?: string; -}; +/** + * Props for {@link HomePageSearchBar}. + * + * @public + */ +export type HomePageSearchBarProps = Partial< + Omit +>; -export const HomePageSearchBar = ({ placeholder }: Props) => { - const [query, setQuery] = React.useState(''); - const handleSearch = useNavigateToQuery(); +/** + * The search bar created specifically for the composable home page + * + * @public + */ +export const HomePageSearchBar = ({ + className: defaultClassName, + ...props +}: HomePageSearchBarProps) => { const classes = useStyles(); + const [query, setQuery] = useState(''); + const handleSearch = useNavigateToQuery(); + + const className = defaultClassName + ? `${classes.searchBar} ${defaultClassName}` + : classes.searchBar; const handleSubmit = () => { handleSearch({ query }); }; - const handleChange = React.useCallback( + const handleChange = useCallback( value => { setQuery(value); }, @@ -50,11 +67,11 @@ export const HomePageSearchBar = ({ placeholder }: Props) => { return ( ); }; diff --git a/plugins/search/src/components/HomePageComponent/index.ts b/plugins/search/src/components/HomePageComponent/index.ts index 2b1cfe27b3..130d04e92f 100644 --- a/plugins/search/src/components/HomePageComponent/index.ts +++ b/plugins/search/src/components/HomePageComponent/index.ts @@ -15,3 +15,4 @@ */ export { HomePageSearchBar } from './HomePageSearchBar'; +export type { HomePageSearchBarProps } from './HomePageSearchBar'; diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx index 4ac4c95224..b3f30473ac 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.test.tsx @@ -101,6 +101,9 @@ describe('SearchBar', () => { }); it('Updates term state when text is entered', async () => { + jest.useFakeTimers(); + const defaultDebounceTime = 200; + render( @@ -116,6 +119,10 @@ describe('SearchBar', () => { userEvent.type(textbox, value); + act(() => { + jest.advanceTimersByTime(defaultDebounceTime); + }); + await waitFor(() => { expect(textbox).toHaveValue(value); }); diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index 693cf98adf..95bf1570e1 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -14,128 +14,145 @@ * limitations under the License. */ -import React, { useEffect, KeyboardEvent, useState } from 'react'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import React, { + ChangeEvent, + KeyboardEvent, + useState, + useEffect, + useCallback, +} from 'react'; import { useDebounce } from 'react-use'; -import { InputBase, InputAdornment, IconButton } from '@material-ui/core'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { + InputBase, + InputBaseProps, + InputAdornment, + IconButton, +} from '@material-ui/core'; import SearchIcon from '@material-ui/icons/Search'; import ClearButton from '@material-ui/icons/Clear'; import { useSearch } from '../SearchContext'; -type PresenterProps = { - value: string; - onChange: (value: string) => void; +/** + * Props for {@link SearchBarBase}. + * + * @public + */ +export type SearchBarBaseProps = Omit & { + debounceTime?: number; + clearButton?: boolean; onClear?: () => void; onSubmit?: () => void; - className?: string; - placeholder?: string; - autoFocus?: boolean; - clearButton?: boolean; + onChange: (value: string) => void; }; +/** + * All search boxes exported by the search plugin are based on the , + * and this one is based on the component from Material UI. + * Recommended if you don't use Search Provider or Search Context. + * + * @public + */ export const SearchBarBase = ({ - autoFocus, - value, onChange, + onKeyDown, onSubmit, - className, - placeholder: overridePlaceholder, + debounceTime = 200, clearButton = true, -}: PresenterProps) => { + fullWidth = true, + value: defaultValue, + inputProps: defaultInputProps = {}, + endAdornment: defaultEndAdornment, + ...props +}: SearchBarBaseProps) => { const configApi = useApi(configApiRef); + const [value, setValue] = useState(defaultValue as string); - const onKeyDown = React.useCallback( + useEffect(() => { + setValue(prevValue => + prevValue !== defaultValue ? (defaultValue as string) : prevValue, + ); + }, [defaultValue]); + + useDebounce(() => onChange(value), debounceTime, [value]); + + const handleChange = useCallback( + (e: ChangeEvent) => { + setValue(e.target.value); + }, + [setValue], + ); + + const handleKeyDown = useCallback( (e: KeyboardEvent) => { + if (onKeyDown) onKeyDown(e); if (onSubmit && e.key === 'Enter') { onSubmit(); } }, - [onSubmit], + [onKeyDown, onSubmit], ); - const handleClear = React.useCallback(() => { + const handleClear = useCallback(() => { onChange(''); }, [onChange]); - const placeholder = - overridePlaceholder ?? - `Search in ${configApi.getOptionalString('app.title') || 'Backstage'}`; + const placeholder = `Search in ${ + configApi.getOptionalString('app.title') || 'Backstage' + }`; + + const startAdornment = ( + + + + + + ); + + const endAdornment = ( + + + + + + ); return ( onChange(e.target.value)} - inputProps={{ 'aria-label': 'Search' }} - startAdornment={ - - - - - - } - endAdornment={ - clearButton && ( - - - - - - ) - } - {...(className && { className })} - {...(onSubmit && { onKeyDown })} + placeholder={placeholder} + startAdornment={startAdornment} + endAdornment={clearButton ? endAdornment : defaultEndAdornment} + inputProps={{ 'aria-label': 'Search', ...defaultInputProps }} + fullWidth={fullWidth} + onChange={handleChange} + onKeyDown={handleKeyDown} + {...props} /> ); }; -type Props = { - autoFocus?: boolean; - className?: string; - debounceTime?: number; - placeholder?: string; - clearButton?: boolean; -}; +/** + * Props for {@link SearchBar}. + * + * @public + */ +export type SearchBarProps = Partial; -export const SearchBar = ({ - autoFocus, - className, - debounceTime = 0, - placeholder, - clearButton = true, -}: Props) => { +/** + * Recommended search bar when you use the Search Provider or Search Context. + * + * @public + */ +export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { const { term, setTerm } = useSearch(); - const [value, setValue] = useState(term); - useEffect(() => { - setValue(prevValue => (prevValue !== term ? term : prevValue)); - }, [term]); - - useDebounce(() => setTerm(value), debounceTime, [value]); - - const handleQuery = (newValue: string) => { - setValue(newValue); + const handleChange = (newValue: string) => { + setTerm(newValue); + if (onChange) onChange(newValue); }; - const handleClear = () => setValue(''); - - return ( - - ); + return ; }; diff --git a/plugins/search/src/components/SearchBar/index.tsx b/plugins/search/src/components/SearchBar/index.tsx index 5adbda282b..f95d2ec0fc 100644 --- a/plugins/search/src/components/SearchBar/index.tsx +++ b/plugins/search/src/components/SearchBar/index.tsx @@ -15,3 +15,4 @@ */ export { SearchBar, SearchBarBase } from './SearchBar'; +export type { SearchBarProps, SearchBarBaseProps } from './SearchBar'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index 75d4b07a3c..ef1eaf650a 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Dialog, DialogActions, @@ -26,7 +26,7 @@ import { } from '@material-ui/core'; import { Launch } from '@material-ui/icons/'; import { makeStyles } from '@material-ui/core/styles'; -import { SearchBarBase } from '../SearchBar'; +import { SearchBar } from '../SearchBar'; import { DefaultResultListItem } from '../DefaultResultListItem'; import { SearchResult } from '../SearchResult'; import { SearchContextProvider, useSearch } from '../SearchContext'; @@ -35,8 +35,6 @@ import { useRouteRef } from '@backstage/core-plugin-api'; import { Link } from '@backstage/core-components'; import { rootRouteRef } from '../../plugin'; -import { useDebounce } from 'react-use'; - export interface SearchModalProps { open?: boolean; toggleModal: () => void; @@ -61,24 +59,10 @@ export const Modal = ({ open = true, toggleModal }: SearchModalProps) => { const getSearchLink = useRouteRef(rootRouteRef); const classes = useStyles(); - const { term, setTerm } = useSearch(); - const [value, setValue] = useState(term); - - useEffect(() => { - setValue(prevValue => (prevValue !== term ? term : prevValue)); - }, [term]); - - useDebounce(() => setTerm(value), 500, [value]); - - const handleQuery = (newValue: string) => { - setValue(newValue); - }; - - const handleClear = () => setValue(''); + const { term } = useSearch(); const handleResultClick = () => { toggleModal(); - handleClear(); }; const handleKeyPress = () => { @@ -98,12 +82,7 @@ export const Modal = ({ open = true, toggleModal }: SearchModalProps) => { > - + @@ -114,10 +93,7 @@ export const Modal = ({ open = true, toggleModal }: SearchModalProps) => { alignItems="center" > - + View Full Results diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx index 6a4eae4001..e413e9505d 100644 --- a/plugins/search/src/components/index.tsx +++ b/plugins/search/src/components/index.tsx @@ -26,3 +26,4 @@ export * from './SearchResultPager'; export * from './SearchType'; export * from './SidebarSearch'; export * from './SidebarSearchModal'; +export * from './HomePageComponent'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index bfc9c52b74..5bfdcf4318 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -26,6 +26,7 @@ export { Filters, FiltersButton, SearchBar, + SearchBarBase, SearchContextProvider, SearchFilter, SearchFilterNext, @@ -39,9 +40,12 @@ export { export type { SearchModalProps, SidebarSearchModalProps, + HomePageSearchBarProps, SidebarSearchProps, + FiltersState, + SearchBarProps, + SearchBarBaseProps, } from './components'; -export type { FiltersState } from './components'; export { DefaultResultListItem, HomePageSearchBar,