refactor: apply review suggestions
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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, makeStyles, Paper } from '@material-ui/core';
|
||||
import LabelIcon from '@material-ui/icons/Label';
|
||||
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
|
||||
import { SearchAutocomplete } from './SearchAutocomplete';
|
||||
import { SearchAutocompleteDefaultOption } from './SearchAutocompleteDefaultOption';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Search/SearchAutocomplete',
|
||||
component: SearchAutocomplete,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<Story />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export const Default = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<SearchAutocomplete options={['hello-word', 'petstore', 'spotify']} />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const Outlined = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.root} variant="outlined">
|
||||
<SearchAutocomplete options={['hello-word', 'petstore', 'spotify']} />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const Initialized = () => {
|
||||
const classes = useStyles();
|
||||
const options = ['hello-word', 'petstore', 'spotify'];
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<SearchAutocomplete options={options} value={options[0]} />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingOptions = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<SearchAutocomplete options={[]} loading />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const RenderingCustomOptions = () => {
|
||||
const classes = useStyles();
|
||||
const options = [
|
||||
{
|
||||
title: 'hello-world',
|
||||
text: 'Hello World example for gRPC',
|
||||
},
|
||||
{
|
||||
title: 'petstore',
|
||||
text: 'The petstore API',
|
||||
},
|
||||
{
|
||||
title: 'spotify',
|
||||
text: 'The Spotify web API',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<SearchAutocomplete
|
||||
options={options}
|
||||
renderOption={option => (
|
||||
<SearchAutocompleteDefaultOption
|
||||
icon={<LabelIcon titleAccess="Option icon" />}
|
||||
primaryText={option.title}
|
||||
secondaryText={option.text}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
@@ -25,10 +25,8 @@ import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { TestApiProvider, renderWithEffects } from '@backstage/test-utils';
|
||||
|
||||
import { searchApiRef } from '../../api';
|
||||
import {
|
||||
SearchAutocomplete,
|
||||
SearchAutocompleteDefaultOption,
|
||||
} from './SearchAutocomplete';
|
||||
import { SearchAutocomplete } from './SearchAutocomplete';
|
||||
import { SearchAutocompleteDefaultOption } from './SearchAutocompleteDefaultOption';
|
||||
|
||||
const title = 'Backstage Test App';
|
||||
const configApiMock = new ConfigReader({
|
||||
@@ -98,7 +96,7 @@ describe('SearchAutocomplete', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toBeCalledWith({
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
@@ -120,7 +118,7 @@ describe('SearchAutocomplete', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toBeCalledWith({
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
@@ -131,7 +129,7 @@ describe('SearchAutocomplete', () => {
|
||||
await userEvent.click(screen.getByLabelText('Clear'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toBeCalledWith({
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: '',
|
||||
@@ -157,7 +155,7 @@ describe('SearchAutocomplete', () => {
|
||||
await userEvent.click(screen.getByText(options[0]));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toBeCalledWith({
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
|
||||
@@ -14,16 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, ReactNode, useCallback, useMemo } from 'react';
|
||||
import React, { ChangeEvent, useCallback, useMemo } from 'react';
|
||||
|
||||
import { CircularProgress } from '@material-ui/core';
|
||||
import {
|
||||
CircularProgress,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListItemTextProps,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
Value,
|
||||
Autocomplete,
|
||||
AutocompleteProps,
|
||||
AutocompleteChangeDetails,
|
||||
@@ -39,38 +33,29 @@ import { SearchBar, SearchBarProps } from '../SearchBar';
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SearchAutocompleteProps<
|
||||
T,
|
||||
Multiple extends boolean | undefined,
|
||||
DisableClearable extends boolean | undefined,
|
||||
FreeSolo extends boolean | undefined,
|
||||
> = Omit<
|
||||
AutocompleteProps<T, Multiple, DisableClearable, FreeSolo>,
|
||||
'renderInput'
|
||||
export type SearchAutocompleteProps<Option> = Omit<
|
||||
AutocompleteProps<Option, undefined, undefined, boolean>,
|
||||
'renderInput' | 'disableClearable' | 'multiple'
|
||||
> & {
|
||||
'data-testid'?: string;
|
||||
inputPlaceholder?: SearchBarProps['placeholder'];
|
||||
inputDebounceTime?: SearchBarProps['debounceTime'];
|
||||
renderInput?: (params: AutocompleteRenderInputParams) => JSX.Element;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for {@link SearchAutocomplete}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SearchAutocompleteComponent = <Option>(
|
||||
props: SearchAutocompleteProps<Option>,
|
||||
) => JSX.Element;
|
||||
|
||||
const withContext = (
|
||||
Component: <
|
||||
T,
|
||||
Multiple extends boolean | undefined,
|
||||
DisableClearable extends boolean | undefined,
|
||||
FreeSolo extends boolean | undefined,
|
||||
>(
|
||||
props: SearchAutocompleteProps<T, Multiple, DisableClearable, FreeSolo>,
|
||||
) => JSX.Element,
|
||||
) => {
|
||||
return <
|
||||
T,
|
||||
Multiple extends boolean | undefined,
|
||||
DisableClearable extends boolean | undefined,
|
||||
FreeSolo extends boolean | undefined,
|
||||
>(
|
||||
props: SearchAutocompleteProps<T, Multiple, DisableClearable, FreeSolo>,
|
||||
) => (
|
||||
<SearchContextProvider useParentContext>
|
||||
Component: SearchAutocompleteComponent,
|
||||
): SearchAutocompleteComponent => {
|
||||
return props => (
|
||||
<SearchContextProvider inheritParentContextIfAvailable>
|
||||
<Component {...props} />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
@@ -82,43 +67,54 @@ const withContext = (
|
||||
* @public
|
||||
*/
|
||||
export const SearchAutocomplete = withContext(
|
||||
<
|
||||
T,
|
||||
Multiple extends boolean | undefined,
|
||||
DisableClearable extends boolean | undefined,
|
||||
FreeSolo extends boolean | undefined,
|
||||
>({
|
||||
loading,
|
||||
value,
|
||||
onChange = () => {},
|
||||
options = [],
|
||||
getOptionLabel = (option: T) => String(option),
|
||||
renderInput,
|
||||
inputDebounceTime,
|
||||
fullWidth = true,
|
||||
clearOnBlur = false,
|
||||
...rest
|
||||
}: SearchAutocompleteProps<T, Multiple, DisableClearable, FreeSolo>) => {
|
||||
function SearchAutocompleteComponent<Option>(
|
||||
props: SearchAutocompleteProps<Option>,
|
||||
) {
|
||||
const {
|
||||
loading,
|
||||
value,
|
||||
onChange = () => {},
|
||||
options = [],
|
||||
getOptionLabel = (option: Option) => String(option),
|
||||
inputPlaceholder,
|
||||
inputDebounceTime,
|
||||
freeSolo = true,
|
||||
fullWidth = true,
|
||||
clearOnBlur = false,
|
||||
'data-testid': dataTestId = 'search-autocomplete',
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const { setTerm } = useSearch();
|
||||
|
||||
const inputValue = useMemo(() => {
|
||||
return value ? getOptionLabel(value as T) : '';
|
||||
}, [value, getOptionLabel]);
|
||||
const getInputValue = useCallback(
|
||||
(option?: null | string | Option) => {
|
||||
if (!option) return '';
|
||||
if (typeof option === 'string') return option;
|
||||
return getOptionLabel(option);
|
||||
},
|
||||
[getOptionLabel],
|
||||
);
|
||||
|
||||
const inputValue = useMemo(
|
||||
() => getInputValue(value),
|
||||
[value, getInputValue],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(
|
||||
event: ChangeEvent<{}>,
|
||||
newValue: Value<T, Multiple, DisableClearable, FreeSolo>,
|
||||
option: null | string | Option,
|
||||
reason: AutocompleteChangeReason,
|
||||
details?: AutocompleteChangeDetails<T>,
|
||||
details?: AutocompleteChangeDetails<Option>,
|
||||
) => {
|
||||
onChange(event, newValue, reason, details);
|
||||
setTerm(newValue ? getOptionLabel(newValue as T) : '');
|
||||
setTerm(getInputValue(option));
|
||||
onChange(event, option, reason, details);
|
||||
},
|
||||
[getOptionLabel, setTerm, onChange],
|
||||
[getInputValue, setTerm, onChange],
|
||||
);
|
||||
|
||||
const defaultRenderInput = useCallback(
|
||||
const renderInput = useCallback(
|
||||
({
|
||||
InputProps: { ref, endAdornment },
|
||||
InputLabelProps,
|
||||
@@ -129,6 +125,7 @@ export const SearchAutocomplete = withContext(
|
||||
ref={ref}
|
||||
clearButton={false}
|
||||
value={inputValue}
|
||||
placeholder={inputPlaceholder}
|
||||
debounceTime={inputDebounceTime}
|
||||
endAdornment={
|
||||
loading ? (
|
||||
@@ -143,60 +140,22 @@ export const SearchAutocomplete = withContext(
|
||||
}
|
||||
/>
|
||||
),
|
||||
[loading, inputValue, inputDebounceTime],
|
||||
[loading, inputValue, inputPlaceholder, inputDebounceTime],
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
data-testid="search-autocomplete"
|
||||
{...rest}
|
||||
data-testid={dataTestId}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
getOptionLabel={getOptionLabel}
|
||||
renderInput={renderInput ?? defaultRenderInput}
|
||||
renderInput={renderInput}
|
||||
freeSolo={freeSolo}
|
||||
fullWidth={fullWidth}
|
||||
clearOnBlur={clearOnBlur}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for {@link SearchAutocompleteDefaultOption}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SearchAutocompleteDefaultOptionProps = {
|
||||
icon?: ReactNode;
|
||||
primaryText: ListItemTextProps['primary'];
|
||||
primaryTextTypographyProps?: ListItemTextProps['primaryTypographyProps'];
|
||||
secondaryText?: ListItemTextProps['secondary'];
|
||||
secondaryTextTypographyProps?: ListItemTextProps['secondaryTypographyProps'];
|
||||
disableTextTypography?: ListItemTextProps['disableTypography'];
|
||||
};
|
||||
|
||||
/**
|
||||
* A default search bar autocomplete component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SearchAutocompleteDefaultOption = ({
|
||||
icon,
|
||||
primaryText,
|
||||
primaryTextTypographyProps,
|
||||
secondaryText,
|
||||
secondaryTextTypographyProps,
|
||||
disableTextTypography,
|
||||
}: SearchAutocompleteDefaultOptionProps) => (
|
||||
<>
|
||||
{icon ? <ListItemIcon>{icon}</ListItemIcon> : null}
|
||||
<ListItemText
|
||||
primary={primaryText}
|
||||
primaryTypographyProps={primaryTextTypographyProps}
|
||||
secondary={secondaryText}
|
||||
secondaryTypographyProps={secondaryTextTypographyProps}
|
||||
disableTypography={disableTextTypography}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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, PropsWithChildren } from 'react';
|
||||
|
||||
import { Grid, ListItem } from '@material-ui/core';
|
||||
import LabelIcon from '@material-ui/icons/Label';
|
||||
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
|
||||
import { SearchAutocompleteDefaultOption } from './SearchAutocompleteDefaultOption';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Search/SearchAutocompleteDefaultOption',
|
||||
component: SearchAutocompleteDefaultOption,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<ListItem>
|
||||
<Story />
|
||||
</ListItem>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<SearchAutocompleteDefaultOption primaryText="hello-world" />
|
||||
);
|
||||
|
||||
export const Icon = () => (
|
||||
<SearchAutocompleteDefaultOption
|
||||
icon={<LabelIcon />}
|
||||
primaryText="hello-world"
|
||||
/>
|
||||
);
|
||||
|
||||
export const SecondaryText = () => (
|
||||
<SearchAutocompleteDefaultOption
|
||||
primaryText="hello-world"
|
||||
secondaryText="Hello World example for gRPC"
|
||||
/>
|
||||
);
|
||||
|
||||
export const AllCombined = () => (
|
||||
<SearchAutocompleteDefaultOption
|
||||
icon={<LabelIcon />}
|
||||
primaryText="hello-world"
|
||||
secondaryText="Hello World example for gRPC"
|
||||
/>
|
||||
);
|
||||
|
||||
export const CustomTextTypographies = () => (
|
||||
<SearchAutocompleteDefaultOption
|
||||
icon={<LabelIcon />}
|
||||
primaryText="hello-world"
|
||||
primaryTextTypographyProps={{ color: 'primary' }}
|
||||
secondaryText="Hello World example for gRPC"
|
||||
secondaryTextTypographyProps={{ color: 'secondary' }}
|
||||
/>
|
||||
);
|
||||
|
||||
const CustomPrimaryText = ({ children }: PropsWithChildren<{}>) => (
|
||||
<dt>{children}</dt>
|
||||
);
|
||||
|
||||
const CustomSecondaryText = ({ children }: PropsWithChildren<{}>) => (
|
||||
<dd>{children}</dd>
|
||||
);
|
||||
|
||||
export const CustomTextComponents = () => (
|
||||
<dl>
|
||||
<SearchAutocompleteDefaultOption
|
||||
icon={<LabelIcon />}
|
||||
primaryText={<CustomPrimaryText>hello-world</CustomPrimaryText>}
|
||||
secondaryText={
|
||||
<CustomSecondaryText>Hello World example for gRPC</CustomSecondaryText>
|
||||
}
|
||||
disableTextTypography
|
||||
/>
|
||||
</dl>
|
||||
);
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
import {
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListItemTextProps,
|
||||
} from '@material-ui/core';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchAutocompleteDefaultOption}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SearchAutocompleteDefaultOptionProps = {
|
||||
icon?: ReactNode;
|
||||
primaryText: ListItemTextProps['primary'];
|
||||
primaryTextTypographyProps?: ListItemTextProps['primaryTypographyProps'];
|
||||
secondaryText?: ListItemTextProps['secondary'];
|
||||
secondaryTextTypographyProps?: ListItemTextProps['secondaryTypographyProps'];
|
||||
disableTextTypography?: ListItemTextProps['disableTypography'];
|
||||
};
|
||||
|
||||
/**
|
||||
* A default search autocomplete option component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SearchAutocompleteDefaultOption = ({
|
||||
icon,
|
||||
primaryText,
|
||||
primaryTextTypographyProps,
|
||||
secondaryText,
|
||||
secondaryTextTypographyProps,
|
||||
disableTextTypography,
|
||||
}: SearchAutocompleteDefaultOptionProps) => (
|
||||
<>
|
||||
{icon ? <ListItemIcon>{icon}</ListItemIcon> : null}
|
||||
<ListItemText
|
||||
primary={primaryText}
|
||||
primaryTypographyProps={primaryTextTypographyProps}
|
||||
secondary={secondaryText}
|
||||
secondaryTypographyProps={secondaryTextTypographyProps}
|
||||
disableTypography={disableTextTypography}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -14,12 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
SearchAutocomplete,
|
||||
SearchAutocompleteDefaultOption,
|
||||
} from './SearchAutocomplete';
|
||||
export { SearchAutocomplete } from './SearchAutocomplete';
|
||||
|
||||
export { SearchAutocompleteDefaultOption } from './SearchAutocompleteDefaultOption';
|
||||
|
||||
export type {
|
||||
SearchAutocompleteProps,
|
||||
SearchAutocompleteDefaultOptionProps,
|
||||
SearchAutocompleteComponent,
|
||||
} from './SearchAutocomplete';
|
||||
|
||||
export type { SearchAutocompleteDefaultOptionProps } from './SearchAutocompleteDefaultOption';
|
||||
|
||||
@@ -22,6 +22,7 @@ import React, {
|
||||
useCallback,
|
||||
forwardRef,
|
||||
ComponentType,
|
||||
ForwardRefExoticComponent,
|
||||
} from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
|
||||
@@ -43,6 +44,14 @@ import {
|
||||
import { SearchContextProvider, useSearch } from '../../context';
|
||||
import { TrackSearch } from '../SearchTracker';
|
||||
|
||||
function withContext<T>(Component: ComponentType<T>) {
|
||||
return forwardRef<unknown, T>((props, ref) => (
|
||||
<SearchContextProvider inheritParentContextIfAvailable>
|
||||
<Component {...props} ref={ref} />
|
||||
</SearchContextProvider>
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for {@link SearchBarBase}.
|
||||
*
|
||||
@@ -63,97 +72,98 @@ export type SearchBarBaseProps = Omit<InputBaseProps, 'onChange'> & {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SearchBarBase = forwardRef<unknown, SearchBarBaseProps>(
|
||||
(
|
||||
{
|
||||
onChange,
|
||||
onKeyDown = () => {},
|
||||
onClear = () => {},
|
||||
onSubmit = () => {},
|
||||
debounceTime = 200,
|
||||
clearButton = true,
|
||||
fullWidth = true,
|
||||
value: defaultValue,
|
||||
inputProps: defaultInputProps = {},
|
||||
endAdornment: defaultEndAdornment,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const [value, setValue] = useState<string>('');
|
||||
export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps> =
|
||||
withContext(
|
||||
forwardRef((props, ref) => {
|
||||
const {
|
||||
onChange,
|
||||
onKeyDown = () => {},
|
||||
onClear = () => {},
|
||||
onSubmit = () => {},
|
||||
debounceTime = 200,
|
||||
clearButton = true,
|
||||
fullWidth = true,
|
||||
value: defaultValue,
|
||||
placeholder: defaultPlaceholder,
|
||||
inputProps: defaultInputProps = {},
|
||||
endAdornment: defaultEndAdornment,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
setValue(prevValue =>
|
||||
prevValue !== defaultValue ? String(defaultValue) : prevValue,
|
||||
const configApi = useApi(configApiRef);
|
||||
const [value, setValue] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
setValue(prevValue =>
|
||||
prevValue !== defaultValue ? String(defaultValue) : prevValue,
|
||||
);
|
||||
}, [defaultValue]);
|
||||
|
||||
useDebounce(() => onChange(value), debounceTime, [value]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
},
|
||||
[setValue],
|
||||
);
|
||||
}, [defaultValue]);
|
||||
|
||||
useDebounce(() => onChange(value), debounceTime, [value]);
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (onKeyDown) onKeyDown(e);
|
||||
if (onSubmit && e.key === 'Enter') {
|
||||
onSubmit();
|
||||
}
|
||||
},
|
||||
[onKeyDown, onSubmit],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
},
|
||||
[setValue],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (onKeyDown) onKeyDown(e);
|
||||
if (onSubmit && e.key === 'Enter') {
|
||||
onSubmit();
|
||||
const handleClear = useCallback(() => {
|
||||
onChange('');
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
},
|
||||
[onKeyDown, onSubmit],
|
||||
);
|
||||
}, [onChange, onClear]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
onChange('');
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
}, [onChange, onClear]);
|
||||
const placeholder =
|
||||
defaultPlaceholder ??
|
||||
`Search in ${configApi.getOptionalString('app.title') || 'Backstage'}`;
|
||||
|
||||
const placeholder = `Search in ${
|
||||
configApi.getOptionalString('app.title') || 'Backstage'
|
||||
}`;
|
||||
const startAdornment = (
|
||||
<InputAdornment position="start">
|
||||
<IconButton aria-label="Query" size="small" disabled>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
|
||||
const startAdornment = (
|
||||
<InputAdornment position="start">
|
||||
<IconButton aria-label="Query" size="small" disabled>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
const endAdornment = (
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label="Clear" size="small" onClick={handleClear}>
|
||||
<ClearButton />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
|
||||
const endAdornment = (
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label="Clear" size="small" onClick={handleClear}>
|
||||
<ClearButton />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
|
||||
return (
|
||||
<TrackSearch>
|
||||
<InputBase
|
||||
data-testid="search-bar-next"
|
||||
ref={ref}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
startAdornment={startAdornment}
|
||||
endAdornment={clearButton ? endAdornment : defaultEndAdornment}
|
||||
inputProps={{ 'aria-label': 'Search', ...defaultInputProps }}
|
||||
fullWidth={fullWidth}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
/>
|
||||
</TrackSearch>
|
||||
);
|
||||
},
|
||||
);
|
||||
return (
|
||||
<TrackSearch>
|
||||
<InputBase
|
||||
data-testid="search-bar-next"
|
||||
ref={ref}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
startAdornment={startAdornment}
|
||||
endAdornment={clearButton ? endAdornment : defaultEndAdornment}
|
||||
inputProps={{ 'aria-label': 'Search', ...defaultInputProps }}
|
||||
fullWidth={fullWidth}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...rest}
|
||||
/>
|
||||
</TrackSearch>
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for {@link SearchBar}.
|
||||
@@ -162,53 +172,45 @@ export const SearchBarBase = forwardRef<unknown, SearchBarBaseProps>(
|
||||
*/
|
||||
export type SearchBarProps = Partial<SearchBarBaseProps>;
|
||||
|
||||
const withContext = (Component: ComponentType<SearchBarProps>) => {
|
||||
return forwardRef<unknown, SearchBarProps>((props, ref) => (
|
||||
<SearchContextProvider useParentContext>
|
||||
<Component {...props} ref={ref} />
|
||||
</SearchContextProvider>
|
||||
));
|
||||
};
|
||||
|
||||
/**
|
||||
* Recommended search bar when you use the Search Provider or Search Context.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SearchBar = withContext(
|
||||
forwardRef<unknown, SearchBarProps>(
|
||||
({ value: initialValue = '', onChange, ...rest }, ref) => {
|
||||
const { term, setTerm } = useSearch();
|
||||
export const SearchBar: ForwardRefExoticComponent<SearchBarProps> = withContext(
|
||||
forwardRef((props, ref) => {
|
||||
const { value: initialValue = '', onChange, ...rest } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValue) {
|
||||
setTerm(String(initialValue));
|
||||
const { term, setTerm } = useSearch();
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValue) {
|
||||
setTerm(String(initialValue));
|
||||
}
|
||||
}, [initialValue, setTerm]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
} else {
|
||||
setTerm(newValue);
|
||||
}
|
||||
}, [initialValue, setTerm]);
|
||||
},
|
||||
[onChange, setTerm],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
} else {
|
||||
setTerm(newValue);
|
||||
}
|
||||
},
|
||||
[onChange, setTerm],
|
||||
);
|
||||
|
||||
return (
|
||||
<AnalyticsContext
|
||||
attributes={{ pluginId: 'search', extension: 'SearchBar' }}
|
||||
>
|
||||
<SearchBarBase
|
||||
{...rest}
|
||||
ref={ref}
|
||||
value={term}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</AnalyticsContext>
|
||||
);
|
||||
},
|
||||
),
|
||||
return (
|
||||
<AnalyticsContext
|
||||
attributes={{ pluginId: 'search', extension: 'SearchBar' }}
|
||||
>
|
||||
<SearchBarBase
|
||||
{...rest}
|
||||
ref={ref}
|
||||
value={term}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</AnalyticsContext>
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -100,11 +100,6 @@ const searchInitialState: SearchContextState = {
|
||||
types: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new local search context.
|
||||
* @remarks Use it for isolating this context from parent search contexts.
|
||||
* @internal
|
||||
*/
|
||||
const useSearchContextValue = (
|
||||
initialValue: SearchContextState = searchInitialState,
|
||||
) => {
|
||||
@@ -165,32 +160,15 @@ const useSearchContextValue = (
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchContextProvider}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SearchContextProviderProps = PropsWithChildren<{
|
||||
export type LocalSearchContextProps = PropsWithChildren<{
|
||||
initialState?: SearchContextState;
|
||||
/**
|
||||
* If true, don't create a child context if there is a parent one already defined.
|
||||
* @remarks Default to false.
|
||||
*/
|
||||
useParentContext?: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Search context provider which gives you access to shared state between search components
|
||||
*/
|
||||
export const SearchContextProvider = (props: SearchContextProviderProps) => {
|
||||
const { initialState, useParentContext, children } = props;
|
||||
const hasParentContext = useSearchContextCheck();
|
||||
const LocalSearchContext = (props: SearchContextProviderProps) => {
|
||||
const { initialState, children } = props;
|
||||
const value = useSearchContextValue(initialState);
|
||||
|
||||
return useParentContext && hasParentContext ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
return (
|
||||
<AnalyticsContext
|
||||
attributes={{ searchTypes: value.types.sort().join(',') }}
|
||||
>
|
||||
@@ -200,3 +178,48 @@ export const SearchContextProvider = (props: SearchContextProviderProps) => {
|
||||
</AnalyticsContext>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchContextProvider}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SearchContextProviderProps =
|
||||
| PropsWithChildren<{
|
||||
/**
|
||||
* State initialized by a local context.
|
||||
*/
|
||||
initialState?: SearchContextState;
|
||||
/**
|
||||
* Do not create an inheritance from the parent, as a new initial state must be defined in a local context.
|
||||
*/
|
||||
inheritParentContextIfAvailable?: never;
|
||||
}>
|
||||
| PropsWithChildren<{
|
||||
/**
|
||||
* Does not accept initial state since it is already initialized by parent context.
|
||||
*/
|
||||
initialState?: never;
|
||||
/**
|
||||
* If true, don't create a child context if there is a parent one already defined.
|
||||
* @remarks Defaults to false.
|
||||
*/
|
||||
inheritParentContextIfAvailable?: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Search context provider which gives you access to shared state between search components
|
||||
*/
|
||||
export const SearchContextProvider = (props: SearchContextProviderProps) => {
|
||||
const { initialState, inheritParentContextIfAvailable, children } = props;
|
||||
const hasParentContext = useSearchContextCheck();
|
||||
|
||||
return hasParentContext && inheritParentContextIfAvailable ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<LocalSearchContext initialState={initialState}>
|
||||
{children}
|
||||
</LocalSearchContext>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user