refactor: apply review suggestions
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -1,21 +1,59 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': patch
|
||||
'@backstage/plugin-search-react': minor
|
||||
---
|
||||
|
||||
Add the term autocomplete functionality to the search bar with a `SearchAutocomplete` component. Additionally, we provide a `SearchAutocompleteDefaultOption` to render options with an icon, a primary text and a secondary text.
|
||||
Provides search autocomplete functionality through a `SearchAutocomplete` component.
|
||||
A `SearchAutocompleteDefaultOption` can also be used to render options with icons, primary texts, and secondary texts.
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
// import { SearchAutocomplete, SearchAutocompleteDefaultOption} from '@backstage/plugin-search-react';
|
||||
<SearchAutocomplete
|
||||
options={options}
|
||||
getOptionLabel={option => option.title}
|
||||
renderOption={option => (
|
||||
<SearchAutocompleteDefaultOption
|
||||
icon={<OptionIcon />}
|
||||
primaryText={option.title}
|
||||
secondaryText={option.text}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
import React, { ChangeEvent, useState, useCallback } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { Grid, Paper } from '@material-ui/core';
|
||||
|
||||
import { Page, Content } from '@backstage/core-components';
|
||||
import { SearchAutocomplete, SearchAutocompleteDefaultOption} from '@backstage/plugin-search-react';
|
||||
|
||||
const OptionsIcon = () => <svg />
|
||||
|
||||
const SearchPage = () => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const options = useAsync(async () => {
|
||||
// Gets and returns autocomplete options
|
||||
}, [inputValue])
|
||||
|
||||
const useCallback((_event: ChangeEvent<{}>, newInputValue: string) => {
|
||||
setInputValue(newInputValue);
|
||||
}, [setInputValue])
|
||||
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Content>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<Paper>
|
||||
<SearchAutocomplete
|
||||
options={options}
|
||||
inputValue={inputValue}
|
||||
inputDebounceTime={100}
|
||||
onInputChange={handleInputChange}
|
||||
getOptionLabel={option => option.title}
|
||||
renderOption={option => (
|
||||
<SearchAutocompleteDefaultOption
|
||||
icon={<OptionIcon />}
|
||||
primaryText={option.title}
|
||||
secondaryText={option.text}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{'/* Filters and results are omitted */'}
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Add `userParentContext` prop to the `SearchContextProvider`, this added property does not create a local context and consumes the parent if it already exists.
|
||||
Use the new `inheritParentContextIfAvailable` search context property in `SearchModal` instead of manually checking if a parent context exists, this conditional statement was previously duplicated in more than one component like in `SearchBar` as well and is now only done in ` SearchContextProvider`.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': minor
|
||||
---
|
||||
|
||||
We noticed a repeated check for the existence of a parent context before creating a child search context in more the one component such as Search Modal and Search Bar and to remove code duplication we extract the conditional to the context provider, now you can use it passing an `inheritParentContextIfAvailable` prop to the `SearchContextProvider`.
|
||||
|
||||
Note: This added property does not create a local context if there is a parent context and in this case, you cannot use it together with `initialState`, it will result in a type error because the parent context is already initialized.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Use the new `SearchAutocomplete` component in the `TechDocsSearch` component to maintain consistency across search experiences and avoid code duplication.
|
||||
@@ -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 = {
|
||||
|
||||
+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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('SearchModal', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(query).toHaveBeenCalledTimes(2);
|
||||
expect(query).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should use parent search context if defined', async () => {
|
||||
@@ -133,7 +133,7 @@ describe('SearchModal', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(query).toHaveBeenCalledTimes(2);
|
||||
expect(query).toHaveBeenCalledTimes(1);
|
||||
await userEvent.keyboard('{Escape}');
|
||||
expect(toggleModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -194,7 +194,7 @@ export const SearchModal = ({
|
||||
hidden={hidden}
|
||||
>
|
||||
{open && (
|
||||
<SearchContextProvider useParentContext>
|
||||
<SearchContextProvider inheritParentContextIfAvailable>
|
||||
{(children && children({ toggleModal })) ?? (
|
||||
<Modal toggleModal={toggleModal} />
|
||||
)}
|
||||
|
||||
@@ -15,29 +15,25 @@
|
||||
*/
|
||||
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { ResultHighlight } from '@backstage/plugin-search-common';
|
||||
import {
|
||||
SearchAutocomplete,
|
||||
SearchContextProvider,
|
||||
useSearch,
|
||||
} from '@backstage/plugin-search-react';
|
||||
import {
|
||||
makeStyles,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
} from '@material-ui/core';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { makeStyles, Paper } from '@material-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { TechDocsSearchResultListItem } from './TechDocsSearchResultListItem';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
bar: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Props for {@link TechDocsSearch}
|
||||
@@ -62,6 +58,13 @@ type TechDocsDoc = {
|
||||
type TechDocsSearchResult = {
|
||||
type: string;
|
||||
document: TechDocsDoc;
|
||||
highlight?: ResultHighlight;
|
||||
};
|
||||
|
||||
const isTechDocsSearchResult = (
|
||||
option: any,
|
||||
): option is TechDocsSearchResult => {
|
||||
return option?.document;
|
||||
};
|
||||
|
||||
const TechDocsSearchBar = (props: TechDocsSearchProps) => {
|
||||
@@ -69,8 +72,6 @@ const TechDocsSearchBar = (props: TechDocsSearchProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
term,
|
||||
setTerm,
|
||||
setFilters,
|
||||
result: { loading, value: searchVal },
|
||||
} = useSearch();
|
||||
@@ -91,10 +92,6 @@ const TechDocsSearchBar = (props: TechDocsSearchProps) => {
|
||||
};
|
||||
}, [loading, searchVal]);
|
||||
|
||||
const [value, setValue] = useState<string>(term);
|
||||
|
||||
useDebounce(() => setTerm(value), debounceTime, [value]);
|
||||
|
||||
// Update the filter context when the entityId changes, e.g. when the search
|
||||
// bar continues to be rendered, navigating between different TechDocs sites.
|
||||
const { kind, name, namespace } = entityId;
|
||||
@@ -109,82 +106,54 @@ const TechDocsSearchBar = (props: TechDocsSearchProps) => {
|
||||
});
|
||||
}, [kind, namespace, name, setFilters]);
|
||||
|
||||
const handleQuery = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!open) {
|
||||
setOpen(true);
|
||||
}
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleSelection = (_: any, selection: TechDocsSearchResult | null) => {
|
||||
if (selection?.document) {
|
||||
const handleSelection = (
|
||||
_: any,
|
||||
selection: TechDocsSearchResult | string | null,
|
||||
) => {
|
||||
if (isTechDocsSearchResult(selection)) {
|
||||
const { location } = selection.document;
|
||||
navigate(location);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
classes={{ root: classes.root }}
|
||||
data-testid="techdocs-search-bar"
|
||||
size="small"
|
||||
open={open}
|
||||
getOptionLabel={() => ''}
|
||||
filterOptions={x => {
|
||||
return x; // This is needed to get renderOption to be called after options change. Bug in material-ui?
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onFocus={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
onChange={handleSelection}
|
||||
blurOnSelect
|
||||
noOptionsText="No results found"
|
||||
value={null}
|
||||
options={options}
|
||||
renderOption={({ document, highlight }) => (
|
||||
<TechDocsSearchResultListItem
|
||||
result={document}
|
||||
lineClamp={3}
|
||||
asListItem={false}
|
||||
asLink={false}
|
||||
title={document.title}
|
||||
highlight={highlight}
|
||||
/>
|
||||
)}
|
||||
loading={loading}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
data-testid="techdocs-search-bar-input"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
placeholder={`Search ${entityTitle || entityId.name} docs`}
|
||||
value={value}
|
||||
onChange={handleQuery}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IconButton aria-label="Query" disabled>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<React.Fragment>
|
||||
{loading ? (
|
||||
<CircularProgress color="inherit" size={20} />
|
||||
) : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</React.Fragment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Paper className={classes.bar} variant="outlined">
|
||||
<SearchAutocomplete
|
||||
classes={{ root: classes.root }}
|
||||
data-testid="techdocs-search-bar"
|
||||
size="small"
|
||||
open={open}
|
||||
getOptionLabel={() => ''}
|
||||
filterOptions={x => {
|
||||
return x; // This is needed to get renderOption to be called after options change. Bug in material-ui?
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onFocus={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
onChange={handleSelection}
|
||||
blurOnSelect
|
||||
noOptionsText="No results found"
|
||||
value={null}
|
||||
options={options}
|
||||
renderOption={({ document, highlight }) => (
|
||||
<TechDocsSearchResultListItem
|
||||
result={document}
|
||||
lineClamp={3}
|
||||
asListItem={false}
|
||||
asLink={false}
|
||||
title={document.title}
|
||||
highlight={highlight}
|
||||
/>
|
||||
)}
|
||||
loading={loading}
|
||||
inputDebounceTime={debounceTime}
|
||||
inputPlaceholder={`Search ${entityTitle || entityId.name} docs`}
|
||||
freeSolo={false}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user