refactor: apply review suggestions

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2022-08-29 15:07:18 +02:00
parent 18f60427f2
commit ca8d5a6eae
16 changed files with 684 additions and 968 deletions
+22 -595
View File
@@ -8,7 +8,7 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { AsyncState } from 'react-use/lib/useAsync';
import { AutocompleteProps } from '@material-ui/lab';
import { AutocompleteRenderInputParams } from '@material-ui/lab';
import { ForwardRefExoticComponent } from 'react';
import { InputBaseProps } from '@material-ui/core';
import { JsonObject } from '@backstage/types';
import { ListItemTextProps } from '@material-ui/core';
@@ -78,13 +78,11 @@ export interface SearchApi {
export const searchApiRef: ApiRef<SearchApi>;
// @public
export const SearchAutocomplete: <
T,
Multiple extends boolean | undefined,
DisableClearable extends boolean | undefined,
FreeSolo extends boolean | undefined,
>(
props: SearchAutocompleteProps<T, Multiple, DisableClearable, FreeSolo>,
export const SearchAutocomplete: SearchAutocompleteComponent;
// @public
export type SearchAutocompleteComponent = <Option>(
props: SearchAutocompleteProps<Option>,
) => JSX.Element;
// @public
@@ -115,596 +113,20 @@ export type SearchAutocompleteFilterProps = SearchFilterComponentProps & {
};
// @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;
};
// @public
export const SearchBar: React_2.ForwardRefExoticComponent<
Pick<
Partial<SearchBarBaseProps>,
| 'required'
| 'type'
| 'error'
| 'id'
| 'name'
| 'color'
| 'margin'
| 'translate'
| 'value'
| 'hidden'
| 'dir'
| 'slot'
| 'style'
| 'title'
| 'accessKey'
| 'draggable'
| 'lang'
| 'className'
| 'prefix'
| 'contentEditable'
| 'inputMode'
| 'tabIndex'
| 'disabled'
| 'autoComplete'
| 'autoFocus'
| 'defaultChecked'
| 'defaultValue'
| 'suppressContentEditableWarning'
| 'suppressHydrationWarning'
| 'contextMenu'
| 'placeholder'
| 'spellCheck'
| 'radioGroup'
| 'role'
| 'about'
| 'datatype'
| 'inlist'
| 'property'
| 'resource'
| 'typeof'
| 'vocab'
| 'autoCapitalize'
| 'autoCorrect'
| 'autoSave'
| 'itemProp'
| 'itemScope'
| 'itemType'
| 'itemID'
| 'itemRef'
| 'results'
| 'security'
| 'unselectable'
| 'is'
| 'aria-activedescendant'
| 'aria-atomic'
| 'aria-autocomplete'
| 'aria-busy'
| 'aria-checked'
| 'aria-colcount'
| 'aria-colindex'
| 'aria-colspan'
| 'aria-controls'
| 'aria-current'
| 'aria-describedby'
| 'aria-details'
| 'aria-disabled'
| 'aria-dropeffect'
| 'aria-errormessage'
| 'aria-expanded'
| 'aria-flowto'
| 'aria-grabbed'
| 'aria-haspopup'
| 'aria-hidden'
| 'aria-invalid'
| 'aria-keyshortcuts'
| 'aria-label'
| 'aria-labelledby'
| 'aria-level'
| 'aria-live'
| 'aria-modal'
| 'aria-multiline'
| 'aria-multiselectable'
| 'aria-orientation'
| 'aria-owns'
| 'aria-placeholder'
| 'aria-posinset'
| 'aria-pressed'
| 'aria-readonly'
| 'aria-relevant'
| 'aria-required'
| 'aria-roledescription'
| 'aria-rowcount'
| 'aria-rowindex'
| 'aria-rowspan'
| 'aria-selected'
| 'aria-setsize'
| 'aria-sort'
| 'aria-valuemax'
| 'aria-valuemin'
| 'aria-valuenow'
| 'aria-valuetext'
| 'readOnly'
| 'rows'
| 'dangerouslySetInnerHTML'
| 'onCopy'
| 'onCopyCapture'
| 'onCut'
| 'onCutCapture'
| 'onPaste'
| 'onPasteCapture'
| 'onCompositionEnd'
| 'onCompositionEndCapture'
| 'onCompositionStart'
| 'onCompositionStartCapture'
| 'onCompositionUpdate'
| 'onCompositionUpdateCapture'
| 'onFocus'
| 'onFocusCapture'
| 'onBlur'
| 'onBlurCapture'
| 'onChange'
| 'onChangeCapture'
| 'onBeforeInput'
| 'onBeforeInputCapture'
| 'onInput'
| 'onInputCapture'
| 'onReset'
| 'onResetCapture'
| 'onSubmit'
| 'onSubmitCapture'
| 'onInvalid'
| 'onInvalidCapture'
| 'onLoad'
| 'onLoadCapture'
| 'onError'
| 'onErrorCapture'
| 'onKeyDown'
| 'onKeyDownCapture'
| 'onKeyPress'
| 'onKeyPressCapture'
| 'onKeyUp'
| 'onKeyUpCapture'
| 'onAbort'
| 'onAbortCapture'
| 'onCanPlay'
| 'onCanPlayCapture'
| 'onCanPlayThrough'
| 'onCanPlayThroughCapture'
| 'onDurationChange'
| 'onDurationChangeCapture'
| 'onEmptied'
| 'onEmptiedCapture'
| 'onEncrypted'
| 'onEncryptedCapture'
| 'onEnded'
| 'onEndedCapture'
| 'onLoadedData'
| 'onLoadedDataCapture'
| 'onLoadedMetadata'
| 'onLoadedMetadataCapture'
| 'onLoadStart'
| 'onLoadStartCapture'
| 'onPause'
| 'onPauseCapture'
| 'onPlay'
| 'onPlayCapture'
| 'onPlaying'
| 'onPlayingCapture'
| 'onProgress'
| 'onProgressCapture'
| 'onRateChange'
| 'onRateChangeCapture'
| 'onSeeked'
| 'onSeekedCapture'
| 'onSeeking'
| 'onSeekingCapture'
| 'onStalled'
| 'onStalledCapture'
| 'onSuspend'
| 'onSuspendCapture'
| 'onTimeUpdate'
| 'onTimeUpdateCapture'
| 'onVolumeChange'
| 'onVolumeChangeCapture'
| 'onWaiting'
| 'onWaitingCapture'
| 'onAuxClick'
| 'onAuxClickCapture'
| 'onClick'
| 'onClickCapture'
| 'onContextMenu'
| 'onContextMenuCapture'
| 'onDoubleClick'
| 'onDoubleClickCapture'
| 'onDrag'
| 'onDragCapture'
| 'onDragEnd'
| 'onDragEndCapture'
| 'onDragEnter'
| 'onDragEnterCapture'
| 'onDragExit'
| 'onDragExitCapture'
| 'onDragLeave'
| 'onDragLeaveCapture'
| 'onDragOver'
| 'onDragOverCapture'
| 'onDragStart'
| 'onDragStartCapture'
| 'onDrop'
| 'onDropCapture'
| 'onMouseDown'
| 'onMouseDownCapture'
| 'onMouseEnter'
| 'onMouseLeave'
| 'onMouseMove'
| 'onMouseMoveCapture'
| 'onMouseOut'
| 'onMouseOutCapture'
| 'onMouseOver'
| 'onMouseOverCapture'
| 'onMouseUp'
| 'onMouseUpCapture'
| 'onSelect'
| 'onSelectCapture'
| 'onTouchCancel'
| 'onTouchCancelCapture'
| 'onTouchEnd'
| 'onTouchEndCapture'
| 'onTouchMove'
| 'onTouchMoveCapture'
| 'onTouchStart'
| 'onTouchStartCapture'
| 'onPointerDown'
| 'onPointerDownCapture'
| 'onPointerMove'
| 'onPointerMoveCapture'
| 'onPointerUp'
| 'onPointerUpCapture'
| 'onPointerCancel'
| 'onPointerCancelCapture'
| 'onPointerEnter'
| 'onPointerEnterCapture'
| 'onPointerLeave'
| 'onPointerLeaveCapture'
| 'onPointerOver'
| 'onPointerOverCapture'
| 'onPointerOut'
| 'onPointerOutCapture'
| 'onGotPointerCapture'
| 'onGotPointerCaptureCapture'
| 'onLostPointerCapture'
| 'onLostPointerCaptureCapture'
| 'onScroll'
| 'onScrollCapture'
| 'onWheel'
| 'onWheelCapture'
| 'onAnimationStart'
| 'onAnimationStartCapture'
| 'onAnimationEnd'
| 'onAnimationEndCapture'
| 'onAnimationIteration'
| 'onAnimationIterationCapture'
| 'onTransitionEnd'
| 'onTransitionEndCapture'
| 'classes'
| 'innerRef'
| 'fullWidth'
| 'inputProps'
| 'inputRef'
| 'multiline'
| 'endAdornment'
| 'inputComponent'
| 'renderSuffix'
| 'rowsMax'
| 'rowsMin'
| 'maxRows'
| 'minRows'
| 'startAdornment'
| 'onClear'
| 'debounceTime'
| 'clearButton'
> &
React_2.RefAttributes<unknown>
>;
export const SearchBar: ForwardRefExoticComponent<SearchBarProps>;
// @public
export const SearchBarBase: React_2.ForwardRefExoticComponent<
Pick<
SearchBarBaseProps,
| 'required'
| 'type'
| 'error'
| 'id'
| 'name'
| 'color'
| 'margin'
| 'translate'
| 'value'
| 'hidden'
| 'dir'
| 'slot'
| 'style'
| 'title'
| 'accessKey'
| 'draggable'
| 'lang'
| 'className'
| 'prefix'
| 'contentEditable'
| 'inputMode'
| 'tabIndex'
| 'disabled'
| 'autoComplete'
| 'autoFocus'
| 'defaultChecked'
| 'defaultValue'
| 'suppressContentEditableWarning'
| 'suppressHydrationWarning'
| 'contextMenu'
| 'placeholder'
| 'spellCheck'
| 'radioGroup'
| 'role'
| 'about'
| 'datatype'
| 'inlist'
| 'property'
| 'resource'
| 'typeof'
| 'vocab'
| 'autoCapitalize'
| 'autoCorrect'
| 'autoSave'
| 'itemProp'
| 'itemScope'
| 'itemType'
| 'itemID'
| 'itemRef'
| 'results'
| 'security'
| 'unselectable'
| 'is'
| 'aria-activedescendant'
| 'aria-atomic'
| 'aria-autocomplete'
| 'aria-busy'
| 'aria-checked'
| 'aria-colcount'
| 'aria-colindex'
| 'aria-colspan'
| 'aria-controls'
| 'aria-current'
| 'aria-describedby'
| 'aria-details'
| 'aria-disabled'
| 'aria-dropeffect'
| 'aria-errormessage'
| 'aria-expanded'
| 'aria-flowto'
| 'aria-grabbed'
| 'aria-haspopup'
| 'aria-hidden'
| 'aria-invalid'
| 'aria-keyshortcuts'
| 'aria-label'
| 'aria-labelledby'
| 'aria-level'
| 'aria-live'
| 'aria-modal'
| 'aria-multiline'
| 'aria-multiselectable'
| 'aria-orientation'
| 'aria-owns'
| 'aria-placeholder'
| 'aria-posinset'
| 'aria-pressed'
| 'aria-readonly'
| 'aria-relevant'
| 'aria-required'
| 'aria-roledescription'
| 'aria-rowcount'
| 'aria-rowindex'
| 'aria-rowspan'
| 'aria-selected'
| 'aria-setsize'
| 'aria-sort'
| 'aria-valuemax'
| 'aria-valuemin'
| 'aria-valuenow'
| 'aria-valuetext'
| 'readOnly'
| 'rows'
| 'dangerouslySetInnerHTML'
| 'onCopy'
| 'onCopyCapture'
| 'onCut'
| 'onCutCapture'
| 'onPaste'
| 'onPasteCapture'
| 'onCompositionEnd'
| 'onCompositionEndCapture'
| 'onCompositionStart'
| 'onCompositionStartCapture'
| 'onCompositionUpdate'
| 'onCompositionUpdateCapture'
| 'onFocus'
| 'onFocusCapture'
| 'onBlur'
| 'onBlurCapture'
| 'onChange'
| 'onChangeCapture'
| 'onBeforeInput'
| 'onBeforeInputCapture'
| 'onInput'
| 'onInputCapture'
| 'onReset'
| 'onResetCapture'
| 'onSubmit'
| 'onSubmitCapture'
| 'onInvalid'
| 'onInvalidCapture'
| 'onLoad'
| 'onLoadCapture'
| 'onError'
| 'onErrorCapture'
| 'onKeyDown'
| 'onKeyDownCapture'
| 'onKeyPress'
| 'onKeyPressCapture'
| 'onKeyUp'
| 'onKeyUpCapture'
| 'onAbort'
| 'onAbortCapture'
| 'onCanPlay'
| 'onCanPlayCapture'
| 'onCanPlayThrough'
| 'onCanPlayThroughCapture'
| 'onDurationChange'
| 'onDurationChangeCapture'
| 'onEmptied'
| 'onEmptiedCapture'
| 'onEncrypted'
| 'onEncryptedCapture'
| 'onEnded'
| 'onEndedCapture'
| 'onLoadedData'
| 'onLoadedDataCapture'
| 'onLoadedMetadata'
| 'onLoadedMetadataCapture'
| 'onLoadStart'
| 'onLoadStartCapture'
| 'onPause'
| 'onPauseCapture'
| 'onPlay'
| 'onPlayCapture'
| 'onPlaying'
| 'onPlayingCapture'
| 'onProgress'
| 'onProgressCapture'
| 'onRateChange'
| 'onRateChangeCapture'
| 'onSeeked'
| 'onSeekedCapture'
| 'onSeeking'
| 'onSeekingCapture'
| 'onStalled'
| 'onStalledCapture'
| 'onSuspend'
| 'onSuspendCapture'
| 'onTimeUpdate'
| 'onTimeUpdateCapture'
| 'onVolumeChange'
| 'onVolumeChangeCapture'
| 'onWaiting'
| 'onWaitingCapture'
| 'onAuxClick'
| 'onAuxClickCapture'
| 'onClick'
| 'onClickCapture'
| 'onContextMenu'
| 'onContextMenuCapture'
| 'onDoubleClick'
| 'onDoubleClickCapture'
| 'onDrag'
| 'onDragCapture'
| 'onDragEnd'
| 'onDragEndCapture'
| 'onDragEnter'
| 'onDragEnterCapture'
| 'onDragExit'
| 'onDragExitCapture'
| 'onDragLeave'
| 'onDragLeaveCapture'
| 'onDragOver'
| 'onDragOverCapture'
| 'onDragStart'
| 'onDragStartCapture'
| 'onDrop'
| 'onDropCapture'
| 'onMouseDown'
| 'onMouseDownCapture'
| 'onMouseEnter'
| 'onMouseLeave'
| 'onMouseMove'
| 'onMouseMoveCapture'
| 'onMouseOut'
| 'onMouseOutCapture'
| 'onMouseOver'
| 'onMouseOverCapture'
| 'onMouseUp'
| 'onMouseUpCapture'
| 'onSelect'
| 'onSelectCapture'
| 'onTouchCancel'
| 'onTouchCancelCapture'
| 'onTouchEnd'
| 'onTouchEndCapture'
| 'onTouchMove'
| 'onTouchMoveCapture'
| 'onTouchStart'
| 'onTouchStartCapture'
| 'onPointerDown'
| 'onPointerDownCapture'
| 'onPointerMove'
| 'onPointerMoveCapture'
| 'onPointerUp'
| 'onPointerUpCapture'
| 'onPointerCancel'
| 'onPointerCancelCapture'
| 'onPointerEnter'
| 'onPointerEnterCapture'
| 'onPointerLeave'
| 'onPointerLeaveCapture'
| 'onPointerOver'
| 'onPointerOverCapture'
| 'onPointerOut'
| 'onPointerOutCapture'
| 'onGotPointerCapture'
| 'onGotPointerCaptureCapture'
| 'onLostPointerCapture'
| 'onLostPointerCaptureCapture'
| 'onScroll'
| 'onScrollCapture'
| 'onWheel'
| 'onWheelCapture'
| 'onAnimationStart'
| 'onAnimationStartCapture'
| 'onAnimationEnd'
| 'onAnimationEndCapture'
| 'onAnimationIteration'
| 'onAnimationIterationCapture'
| 'onTransitionEnd'
| 'onTransitionEndCapture'
| 'classes'
| 'innerRef'
| 'fullWidth'
| 'inputProps'
| 'inputRef'
| 'multiline'
| 'endAdornment'
| 'inputComponent'
| 'renderSuffix'
| 'rowsMax'
| 'rowsMin'
| 'maxRows'
| 'minRows'
| 'startAdornment'
| 'onClear'
| 'debounceTime'
| 'clearButton'
> &
React_2.RefAttributes<unknown>
>;
export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps>;
// @public
export type SearchBarBaseProps = Omit<InputBaseProps, 'onChange'> & {
@@ -724,10 +146,15 @@ export const SearchContextProvider: (
) => JSX.Element;
// @public
export type SearchContextProviderProps = PropsWithChildren<{
initialState?: SearchContextState;
useParentContext?: boolean;
}>;
export type SearchContextProviderProps =
| PropsWithChildren<{
initialState?: SearchContextState;
inheritParentContextIfAvailable?: never;
}>
| PropsWithChildren<{
initialState?: never;
inheritParentContextIfAvailable?: boolean;
}>;
// @public (undocumented)
export type SearchContextState = {
@@ -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}
/>
</>
);
@@ -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>
);
@@ -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>
);
};