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