1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-09 01:17:06 +02:00

feat: Search suggestion selectable (#4610)

This commit is contained in:
Mateusz Kwasniewski 2023-09-05 15:31:31 +02:00 committed by GitHub
parent 47a59224bb
commit 41858a4952
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 96 additions and 10 deletions

View File

@ -7,6 +7,7 @@ import { SearchSuggestions } from './SearchSuggestions/SearchSuggestions';
import { IGetSearchContextOutput } from 'hooks/useSearch'; import { IGetSearchContextOutput } from 'hooks/useSearch';
import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut'; import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut';
import { SEARCH_INPUT } from 'utils/testIds'; import { SEARCH_INPUT } from 'utils/testIds';
import { useOnClickOutside } from 'hooks/useOnClickOutside';
interface ISearchProps { interface ISearchProps {
initialValue?: string; initialValue?: string;
@ -69,8 +70,10 @@ export const Search = ({
containerStyles, containerStyles,
debounceTime = 200, debounceTime = 200,
}: ISearchProps) => { }: ISearchProps) => {
const ref = useRef<HTMLInputElement>(); const searchInputRef = useRef<HTMLInputElement>(null);
const suggestionsRef = useRef<HTMLInputElement>(null);
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
const hideSuggestions = () => setShowSuggestions(false);
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
const debouncedOnChange = useAsyncDebounce(onChange, debounceTime); const debouncedOnChange = useAsyncDebounce(onChange, debounceTime);
@ -83,20 +86,23 @@ export const Search = ({
const hotkey = useKeyboardShortcut( const hotkey = useKeyboardShortcut(
{ modifiers: ['ctrl'], key: 'k', preventDefault: true }, { modifiers: ['ctrl'], key: 'k', preventDefault: true },
() => { () => {
if (document.activeElement === ref.current) { if (document.activeElement === searchInputRef.current) {
ref.current?.blur(); searchInputRef.current?.blur();
} else { } else {
ref.current?.focus(); searchInputRef.current?.focus();
} }
} }
); );
useKeyboardShortcut({ key: 'Escape' }, () => { useKeyboardShortcut({ key: 'Escape' }, () => {
if (document.activeElement === ref.current) { if (document.activeElement === searchInputRef.current) {
ref.current?.blur(); searchInputRef.current?.blur();
hideSuggestions();
} }
}); });
const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`; const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`;
useOnClickOutside([searchInputRef, suggestionsRef], hideSuggestions);
return ( return (
<StyledContainer style={containerStyles}> <StyledContainer style={containerStyles}>
<StyledSearch className={className}> <StyledSearch className={className}>
@ -107,7 +113,7 @@ export const Search = ({
}} }}
/> />
<StyledInputBase <StyledInputBase
inputRef={ref} inputRef={searchInputRef}
placeholder={placeholder} placeholder={placeholder}
inputProps={{ inputProps={{
'aria-label': placeholder, 'aria-label': placeholder,
@ -116,7 +122,6 @@ export const Search = ({
value={value} value={value}
onChange={e => onSearchChange(e.target.value)} onChange={e => onSearchChange(e.target.value)}
onFocus={() => setShowSuggestions(true)} onFocus={() => setShowSuggestions(true)}
onBlur={() => setShowSuggestions(false)}
disabled={disabled} disabled={disabled}
/> />
<Box sx={{ width: theme => theme.spacing(4) }}> <Box sx={{ width: theme => theme.spacing(4) }}>
@ -128,7 +133,7 @@ export const Search = ({
size="small" size="small"
onClick={() => { onClick={() => {
onSearchChange(''); onSearchChange('');
ref.current?.focus(); searchInputRef.current?.focus();
}} }}
sx={{ padding: theme => theme.spacing(1) }} sx={{ padding: theme => theme.spacing(1) }}
> >
@ -142,7 +147,11 @@ export const Search = ({
<ConditionallyRender <ConditionallyRender
condition={Boolean(hasFilters) && showSuggestions} condition={Boolean(hasFilters) && showSuggestions}
show={ show={
<SearchSuggestions getSearchContext={getSearchContext!} /> <div ref={suggestionsRef}>
<SearchSuggestions
getSearchContext={getSearchContext!}
/>
</div>
} }
/> />
</StyledContainer> </StyledContainer>

View File

@ -0,0 +1,44 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { useRef } from 'react';
import { useOnClickOutside } from './useOnClickOutside';
function TestComponent(props: { outsideClickHandler: () => void }) {
const divRef = useRef(null);
useOnClickOutside([divRef], props.outsideClickHandler);
return (
<div data-testid="wrapper">
<div data-testid="inside" ref={divRef}>
Inside
</div>
<div data-testid="outside">Outside</div>
</div>
);
}
test('should not call the callback when clicking inside', () => {
let mockCallbackCallCount = 0;
const mockCallback = () => mockCallbackCallCount++;
render(<TestComponent outsideClickHandler={mockCallback} />);
const insideDiv = screen.getByTestId('inside');
// Simulate a click inside the div
fireEvent.click(insideDiv);
expect(mockCallbackCallCount).toBe(0);
});
test('should call the callback when clicking outside', () => {
let mockCallbackCallCount = 0;
const mockCallback = () => mockCallbackCallCount++;
render(<TestComponent outsideClickHandler={mockCallback} />);
const outsideDiv = screen.getByTestId('outside');
fireEvent.click(outsideDiv);
expect(mockCallbackCallCount).toBe(1);
});

View File

@ -0,0 +1,33 @@
import { useEffect } from 'react';
/**
* Hook to handle outside clicks for a given list of refs.
*
* @param {Array<React.RefObject>} refs - List of refs to the target elements.
* @param {Function} callback - Callback to execute on outside click.
*/
export const useOnClickOutside = (
refs: Array<React.RefObject<HTMLElement>>,
callback: Function
) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// Check if event target is outside all provided refs
if (
!refs.some(
ref =>
ref.current &&
ref.current.contains(event.target as Node)
)
) {
callback();
}
};
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [refs, callback]);
};