Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-08-23 14:31:10 +02:00
parent e21321d0bc
commit f26ff99a2f
4 changed files with 145 additions and 129 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-react': patch
---
Slight type tweak to match newer React versions better
@@ -77,7 +77,8 @@ describe('<ErrorBoundary/>', () => {
/^The above error occurred in the <Bomb> component:/,
),
expect.stringMatching(/^ErrorBoundary/),
expect.stringMatching(/Warning: findDOMNode/), // React warning, unfortunate but currently true
]);
expect(error.length).toEqual(4);
expect(error.length).toEqual(5);
});
});
+15 -3
View File
@@ -9,7 +9,6 @@ import { ApiRef } from '@backstage/core-plugin-api';
import { AsyncState } from 'react-use/esm/useAsync';
import { AutocompleteProps } from '@material-ui/lab/Autocomplete';
import { Extension } from '@backstage/core-plugin-api';
import { ForwardRefExoticComponent } from 'react';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { LinkProps } from '@backstage/core-components';
@@ -132,10 +131,23 @@ export type SearchAutocompleteProps<Option> = Omit<
};
// @public
export const SearchBar: ForwardRefExoticComponent<SearchBarProps>;
export const SearchBar: React_2.ForwardRefExoticComponent<
Omit<
Omit<Partial<SearchBarBaseProps>, 'ref'> &
React_2.RefAttributes<HTMLDivElement>,
'ref'
> &
React_2.RefAttributes<HTMLDivElement>
>;
// @public
export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps>;
export const SearchBarBase: React_2.ForwardRefExoticComponent<
Omit<
Omit<SearchBarBaseProps, 'ref'> & React_2.RefAttributes<HTMLDivElement>,
'ref'
> &
React_2.RefAttributes<HTMLDivElement>
>;
// @public
export type SearchBarBaseProps = Omit<TextFieldProps, 'onChange'> & {
@@ -30,7 +30,6 @@ import React, {
ChangeEvent,
ComponentType,
forwardRef,
ForwardRefExoticComponent,
KeyboardEvent,
useCallback,
useEffect,
@@ -70,138 +69,137 @@ export type SearchBarBaseProps = Omit<TextFieldProps, 'onChange'> & {
*
* @public
*/
export const SearchBarBase: ForwardRefExoticComponent<SearchBarBaseProps> =
withContext(
forwardRef((props, ref) => {
const {
onChange,
onKeyDown = () => {},
onClear = () => {},
onSubmit = () => {},
debounceTime = 200,
clearButton = true,
fullWidth = true,
value: defaultValue,
label,
placeholder,
inputProps = {},
InputProps = {},
endAdornment,
...rest
} = props;
export const SearchBarBase = withContext(
forwardRef<HTMLDivElement, SearchBarBaseProps>((props, ref) => {
const {
onChange,
onKeyDown = () => {},
onClear = () => {},
onSubmit = () => {},
debounceTime = 200,
clearButton = true,
fullWidth = true,
value: defaultValue,
label,
placeholder,
inputProps = {},
InputProps = {},
endAdornment,
...rest
} = props;
const configApi = useApi(configApiRef);
const [value, setValue] = useState<string>('');
const forwardedValueRef = useRef<string>('');
const configApi = useApi(configApiRef);
const [value, setValue] = useState<string>('');
const forwardedValueRef = useRef<string>('');
useEffect(() => {
setValue(prevValue => {
// We only update the value if our current value is the same as it was
// for the most recent onChange call. Otherwise it means that the users
// has continued typing and we should not replace their input.
if (prevValue === forwardedValueRef.current) {
return String(defaultValue);
}
return prevValue;
});
}, [defaultValue, forwardedValueRef]);
useDebounce(
() => {
forwardedValueRef.current = value;
onChange(value);
},
debounceTime,
[value],
);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
},
[setValue],
);
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLDivElement>) => {
if (onKeyDown) onKeyDown(e);
if (onSubmit && e.key === 'Enter') {
onSubmit();
}
},
[onKeyDown, onSubmit],
);
const handleClear = useCallback(() => {
forwardedValueRef.current = '';
onChange('');
setValue('');
if (onClear) {
onClear();
useEffect(() => {
setValue(prevValue => {
// We only update the value if our current value is the same as it was
// for the most recent onChange call. Otherwise it means that the users
// has continued typing and we should not replace their input.
if (prevValue === forwardedValueRef.current) {
return String(defaultValue);
}
}, [onChange, onClear]);
return prevValue;
});
}, [defaultValue, forwardedValueRef]);
const ariaLabel: string | undefined = label ? undefined : 'Search';
useDebounce(
() => {
forwardedValueRef.current = value;
onChange(value);
},
debounceTime,
[value],
);
const inputPlaceholder =
placeholder ??
`Search in ${configApi.getOptionalString('app.title') || 'Backstage'}`;
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
},
[setValue],
);
const SearchIcon = useApp().getSystemIcon('search') || DefaultSearchIcon;
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLDivElement>) => {
if (onKeyDown) onKeyDown(e);
if (onSubmit && e.key === 'Enter') {
onSubmit();
}
},
[onKeyDown, onSubmit],
);
const startAdornment = (
<InputAdornment position="start">
<IconButton aria-label="Query" size="small" disabled>
<SearchIcon />
</IconButton>
</InputAdornment>
);
const handleClear = useCallback(() => {
forwardedValueRef.current = '';
onChange('');
setValue('');
if (onClear) {
onClear();
}
}, [onChange, onClear]);
const clearButtonEndAdornment = (
<InputAdornment position="end">
<Button
aria-label="Clear"
size="small"
onClick={handleClear}
onKeyDown={event => {
if (event.key === 'Enter') {
// write your functionality here
event.stopPropagation();
}
}}
>
Clear
</Button>
</InputAdornment>
);
const ariaLabel: string | undefined = label ? undefined : 'Search';
return (
<TextField
id="search-bar-text-field"
data-testid="search-bar-next"
variant="outlined"
margin="normal"
inputRef={ref}
value={value}
label={label}
placeholder={inputPlaceholder}
InputProps={{
startAdornment,
endAdornment: clearButton ? clearButtonEndAdornment : endAdornment,
...InputProps,
const inputPlaceholder =
placeholder ??
`Search in ${configApi.getOptionalString('app.title') || 'Backstage'}`;
const SearchIcon = useApp().getSystemIcon('search') || DefaultSearchIcon;
const startAdornment = (
<InputAdornment position="start">
<IconButton aria-label="Query" size="small" disabled>
<SearchIcon />
</IconButton>
</InputAdornment>
);
const clearButtonEndAdornment = (
<InputAdornment position="end">
<Button
aria-label="Clear"
size="small"
onClick={handleClear}
onKeyDown={event => {
if (event.key === 'Enter') {
// write your functionality here
event.stopPropagation();
}
}}
inputProps={{
'aria-label': ariaLabel,
...inputProps,
}}
fullWidth={fullWidth}
onChange={handleChange}
onKeyDown={handleKeyDown}
{...rest}
/>
);
}),
);
>
Clear
</Button>
</InputAdornment>
);
return (
<TextField
id="search-bar-text-field"
data-testid="search-bar-next"
variant="outlined"
margin="normal"
inputRef={ref}
value={value}
label={label}
placeholder={inputPlaceholder}
InputProps={{
startAdornment,
endAdornment: clearButton ? clearButtonEndAdornment : endAdornment,
...InputProps,
}}
inputProps={{
'aria-label': ariaLabel,
...inputProps,
}}
fullWidth={fullWidth}
onChange={handleChange}
onKeyDown={handleKeyDown}
{...rest}
/>
);
}),
);
/**
* Props for {@link SearchBar}.
@@ -215,8 +213,8 @@ export type SearchBarProps = Partial<SearchBarBaseProps>;
*
* @public
*/
export const SearchBar: ForwardRefExoticComponent<SearchBarProps> = withContext(
forwardRef((props, ref) => {
export const SearchBar = withContext(
forwardRef<HTMLDivElement, SearchBarProps>((props, ref) => {
const { value: initialValue = '', onChange, ...rest } = props;
const { term, setTerm } = useSearch();