Skip to content

Commit

Permalink
feat: Search suggestion selectable (#4610)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Sep 5, 2023
1 parent 47a5922 commit 41858a4
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 10 deletions.
29 changes: 19 additions & 10 deletions frontend/src/component/common/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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}>
Expand All @@ -107,7 +113,7 @@ export const Search = ({
}}
/>
<StyledInputBase
inputRef={ref}
inputRef={searchInputRef}
placeholder={placeholder}
inputProps={{
'aria-label': placeholder,
Expand All @@ -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) }}>
Expand All @@ -128,7 +133,7 @@ export const Search = ({
size="small"
onClick={() => {
onSearchChange('');
ref.current?.focus();
searchInputRef.current?.focus();
}}
sx={{ padding: theme => theme.spacing(1) }}
>
Expand All @@ -142,7 +147,11 @@ export const Search = ({
<ConditionallyRender
condition={Boolean(hasFilters) && showSuggestions}
show={
<SearchSuggestions getSearchContext={getSearchContext!} />
<div ref={suggestionsRef}>
<SearchSuggestions
getSearchContext={getSearchContext!}
/>
</div>
}
/>
</StyledContainer>
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/hooks/useOnClickOutside.test.tsx
Original file line number Diff line number Diff line change
@@ -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);
});
33 changes: 33 additions & 0 deletions frontend/src/hooks/useOnClickOutside.ts
Original file line number Diff line number Diff line change
@@ -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]);
};

0 comments on commit 41858a4

Please sign in to comment.