Add initial <Search.Autocomplete /> implementation.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-12-30 15:03:46 +01:00
parent d4d48e1b1a
commit bb20c30a7d
5 changed files with 167 additions and 0 deletions
@@ -0,0 +1,105 @@
/*
* Copyright 2021 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, { ChangeEvent, useState } from 'react';
import { Chip, TextField } from '@material-ui/core';
import {
Autocomplete,
AutocompleteGetTagProps,
AutocompleteRenderInputParams,
} from '@material-ui/lab';
import { useSearch } from '../SearchContext';
import { useAsyncFilterValues } from './hooks';
import { SearchFilterComponentProps } from './SearchFilter';
/**
* @public
*/
export type SearchAutocompleteFilterProps = SearchFilterComponentProps & {
multiple?: boolean;
limitTags?: number;
};
export const AutocompleteFilter = (props: SearchAutocompleteFilterProps) => {
const {
asyncValues,
asyncDebounce,
className,
multiple,
name,
values: givenValues,
label,
limitTags,
} = props;
const [inputValue, setInputValue] = useState<string>('');
const { value: values, loading } = useAsyncFilterValues(
asyncValues,
inputValue,
givenValues,
asyncDebounce,
);
const { setFilters } = useSearch();
// Set new filter values on input change.
const handleChange = (
_: ChangeEvent<{}>,
newValue: string | string[] | null,
) => {
setFilters(prevState => {
const { [name]: filter, ...others } = prevState;
if (newValue) {
return { ...others, [name]: newValue };
}
return { ...others };
});
};
// Provide the input field.
const renderInput = (params: AutocompleteRenderInputParams) => (
<TextField
{...params}
name="search"
variant="outlined"
label={label || name}
fullWidth
/>
);
// Render tags as primary-colored chips.
const renderTags = (
tagValue: string[],
getTagProps: AutocompleteGetTagProps,
) =>
tagValue.map((option: string, index: number) => (
<Chip label={option} color="primary" {...getTagProps({ index })} />
));
return (
<Autocomplete
multiple={multiple}
className={className}
id={`${multiple ?? 'multi-'}select-filter-${name}--select`}
options={values || []}
loading={loading}
limitTags={limitTags}
onChange={handleChange}
onInputChange={(_, newValue) => setInputValue(newValue)}
renderInput={renderInput}
renderTags={renderTags}
/>
);
};
@@ -72,3 +72,49 @@ export const AsyncSelectFilter = () => {
</Paper>
);
};
export const Autocomplete = () => {
return (
<Paper style={{ padding: 10 }}>
<SearchFilter.Autocomplete
name="autocomplete"
label="Single-Select Autocomplete Filter"
values={['abba', 'cadaver']}
/>
</Paper>
);
};
export const MultiSelectAutocomplete = () => {
return (
<Paper style={{ padding: 10 }}>
<SearchFilter.Autocomplete
multiple
name="autocomplete"
label="Multi-Select Autocomplete Filter"
values={['abba', 'cadaver']}
/>
</Paper>
);
};
export const AsyncMultiSelectAutocomplete = () => {
return (
<Paper style={{ padding: 10 }}>
<SearchFilter.Autocomplete
multiple
name="starwarsPerson"
label="Starwars Character"
asyncValues={async partial => {
if (partial === '') return [];
const response = await fetch(
`https://swapi.dev/api/people?search=${partial}`,
);
const json: { results: Array<{ name: string }> } =
await response.json();
return json.results.map(r => r.name);
}}
/>
</Paper>
);
};
@@ -26,6 +26,10 @@ import {
FormLabel,
} from '@material-ui/core';
import {
AutocompleteFilter,
SearchAutocompleteFilterProps,
} from './SearchFilter.Autocomplete';
import { useSearch } from '../SearchContext';
import { useAsyncFilterValues } from './hooks';
@@ -196,6 +200,16 @@ SearchFilter.Select = (
SearchFilterComponentProps,
) => <SearchFilter {...props} component={SelectFilter} />;
/**
* A control surface for a given filter field name, rendered as an autocomplete
* textfield. A hard-coded list of values may be provided, or an async function
* which returns values may be provided instead.
* @public
*/
SearchFilter.Autocomplete = (props: SearchAutocompleteFilterProps) => (
<SearchFilter {...props} component={AutocompleteFilter} />
);
/**
* @deprecated This component was used for rapid prototyping of the Backstage
* Search platform. Now that the API has stabilized, you should use the
@@ -19,3 +19,4 @@ export type {
SearchFilterComponentProps,
SearchFilterWrapperProps,
} from './SearchFilter';
export type { SearchAutocompleteFilterProps } from './SearchFilter.Autocomplete';
+1
View File
@@ -34,6 +34,7 @@ export type {
export { SearchContextProvider, useSearch } from './components/SearchContext';
export { SearchFilter, SearchFilterNext } from './components/SearchFilter';
export type {
SearchAutocompleteFilterProps,
SearchFilterComponentProps,
SearchFilterWrapperProps,
} from './components/SearchFilter';