Replace search view with drop down

Summary:
The support form currently has a search form to select a group, but unless the selection is cleared, it won't show you which groups is actually available, which makes it hard for people to select the right group if they don't know up front.

Since the scale of available groups doesn't justify needing a typeahead, converted it to an ordinary dropdown.

An added benefit is that this allows us to remove a large and complicated component we shouldn't be maintaining ourselves, but rather reuse from Ant.

Reviewed By: nikoant

Differential Revision: D26046131

fbshipit-source-id: f499e5848eec8b961b054104c8e3a01567e2801e
This commit is contained in:
Michel Weststrate
2021-01-26 05:26:02 -08:00
committed by Facebook GitHub Bot
parent de60b28cc7
commit f6d8b19001
2 changed files with 0 additions and 395 deletions

View File

@@ -1,229 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
SearchInput,
styled,
colors,
SearchIcon,
SearchBox,
FlexColumn,
FlexRow,
Text,
Tooltip,
Line,
Spacer,
Glyph,
} from '../ui';
import React, {useState, useCallback, useEffect, useRef} from 'react';
const RowComponentContainer = styled(FlexRow)({
height: '24px',
margin: '4px',
alignItems: 'center',
flexShrink: 0,
});
const Separator = styled(Line)({margin: '2px 0px'});
const OverlayContainer = styled.div({
height: '100%',
overflow: 'visible',
position: 'absolute',
top: '30px',
left: '0px',
width: '100%',
zIndex: 100,
});
const ListViewContainer = styled(FlexColumn)({
borderWidth: '0px 1px 1px',
borderStyle: 'solid',
borderColor: colors.greyTint2,
margin: '0px 10px',
maxHeight: '300px',
backgroundColor: colors.white,
});
const StyledSearchInput = styled(SearchInput)({
height: '20px',
margin: '4px',
});
type Element = {id: string; label: string};
type Props = {
list: Array<Element>;
onSelect?: (id: string, label: string) => void;
handleNoResults?: (value: string) => void;
selectedElementID?: string;
};
function RowComponent(props: {
elem: Element;
onClick: (id: string) => void;
selected?: boolean;
}) {
return (
<RowComponentContainer
data-testid={'row-component'}
onClick={() => {
props.onClick(props.elem.id);
}}>
<Text>{props.elem.label}</Text>
<Spacer />
{props.selected && (
<Glyph color={colors.highlightTint15} name="checkmark" />
)}
</RowComponentContainer>
);
}
export default function (props: Props) {
const {list, handleNoResults, onSelect, selectedElementID} = props;
const [filteredElements, setFilteredElements] = useState<Array<Element>>([]);
const [searchedValue, setSearchedValue] = useState<string>('');
const [selectedElement, setSelectedElement] = useState<Element | undefined>(
undefined,
);
const [focussed, setFocus] = useState<boolean>(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const onChangeCallBack = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearchedValue(e.target.value.trim());
},
[setSearchedValue],
);
const onSelectCallBack = useCallback(
(id: string) => {
const elem = list.find((e) => {
return e.id === id;
});
if (elem) {
setSearchedValue(elem.label);
setSelectedElement(elem);
}
setFocus(false);
if (elem && onSelect) {
onSelect(elem.id, elem.label);
}
},
[list, onSelect, setSearchedValue],
);
const onFocusCallBack = useCallback(
(_e: React.FocusEvent<HTMLInputElement>) => {
setFocus(true);
},
[setFocus],
);
// Set the searched value and selectedElement when the selectedElementID changes.
useEffect(() => {
const initialElement = list.find((e) => e.id === selectedElementID);
if (initialElement) {
setSearchedValue(initialElement.label);
}
setSelectedElement(initialElement);
setFocus(false);
}, [selectedElementID, list]);
// Effect to filter items
useEffect(() => {
if (searchedValue.length > 0) {
const filteredValues = list.filter((s) => {
return s.label.toLowerCase().includes(searchedValue.toLowerCase());
});
if (filteredValues.length === 0 && handleNoResults) {
handleNoResults(searchedValue);
}
setFilteredElements(filteredValues);
} else if (focussed) {
setFilteredElements(list);
}
}, [searchedValue, handleNoResults, list, setFilteredElements, focussed]);
// Effect to detect outside click
useEffect(() => {
//TODO: Generalise this effect so that other components can use it.
function handleClickOutside(event: MouseEvent) {
const current = wrapperRef.current;
const target = event.target;
if (
wrapperRef &&
current &&
target &&
!current.contains(target as Node) &&
focussed
) {
if (selectedElement && onSelect) {
setSearchedValue(selectedElement.label);
onSelect(selectedElement.id, selectedElement.label);
}
setFocus(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [
wrapperRef,
selectedElement,
onSelect,
setSearchedValue,
setFocus,
focussed,
]);
const validationError =
filteredElements.length === 0 && searchedValue.length > 0
? 'Unsupported element, please try clearing your text to see the list of elements.'
: '';
return (
<FlexColumn ref={wrapperRef}>
<Tooltip title={validationError} options={{position: 'below'}}>
<SearchBox isInvalidInput={validationError.length > 0}>
<SearchIcon
name="magnifying-glass"
color={colors.macOSTitleBarIcon}
size={16}
/>
<StyledSearchInput
placeholder={'Search Groups'}
onChange={onChangeCallBack}
onFocus={onFocusCallBack}
value={searchedValue}
isValidInput={false}
data-testid={'search-input'}
/>
</SearchBox>
</Tooltip>
{filteredElements.length > 0 && focussed && (
<OverlayContainer>
<ListViewContainer scrollable={true}>
{filteredElements.map((e, idx) => {
return (
<FlexColumn key={idx}>
<RowComponent
elem={e}
onClick={onSelectCallBack}
selected={selectedElement && e.id == selectedElement.id}
/>
{idx < filteredElements.length - 1 && <Separator />}
</FlexColumn>
);
})}
</ListViewContainer>
</OverlayContainer>
)}
</FlexColumn>
);
}

View File

@@ -1,166 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import * as React from 'react';
import {render, fireEvent} from '@testing-library/react';
import DropDownSearchView from '../DropDownSearchView';
import {act} from 'react-dom/test-utils';
test('Test selected element id is shown as the selected one.', async () => {
const res = render(
<DropDownSearchView
list={[{id: 'id1', label: 'label1'}]}
onSelect={jest.fn()}
handleNoResults={jest.fn()}
selectedElementID={'id1'}
/>,
);
const searchInput = (await res.findByTestId(
'search-input',
)) as HTMLInputElement;
expect(searchInput).toBeTruthy();
expect(searchInput.value).toEqual('label1');
searchInput.focus();
});
test('Test the change of the selectedElementID changes the the selected element in the UI.', async () => {
const res = render(
<DropDownSearchView
list={[
{id: 'id1', label: 'label1'},
{id: 'id2', label: 'label2'},
]}
onSelect={jest.fn()}
handleNoResults={jest.fn()}
selectedElementID={'id1'}
/>,
);
const searchInput = (await res.findByTestId(
'search-input',
)) as HTMLInputElement;
expect(searchInput).toBeTruthy();
expect(searchInput.value).toEqual('label1');
res.rerender(
<DropDownSearchView
list={[
{id: 'id1', label: 'label1'},
{id: 'id2', label: 'label2'},
]}
onSelect={jest.fn()}
handleNoResults={jest.fn()}
selectedElementID={'id2'}
/>,
);
const searchInputRerendered = (await res.findByTestId(
'search-input',
)) as HTMLInputElement;
expect(searchInputRerendered).toBeTruthy();
expect(searchInputRerendered.value).toEqual('label2');
});
test('Test the entire flow and click on the available options.', async () => {
const onSelect = jest.fn();
const res = render(
<DropDownSearchView
list={[
{id: 'id1', label: 'label1'},
{id: 'id2', label: 'label2'},
{id: 'id3', label: 'label3'},
{id: 'id4', label: 'label4'},
]}
onSelect={onSelect}
handleNoResults={jest.fn()}
selectedElementID={'id1'}
/>,
);
const searchInput = (await res.findByTestId(
'search-input',
)) as HTMLInputElement;
expect(searchInput).toBeTruthy();
expect(searchInput.value).toEqual('label1');
searchInput.focus();
// Right now just the filtered elements will show up
expect(res.queryByText('label1')).toBeTruthy();
expect(res.queryByText('label2')).toBeFalsy();
expect(res.queryByText('label3')).toBeFalsy();
expect(res.queryByText('label4')).toBeFalsy();
act(() => {
fireEvent.change(searchInput, {target: {value: ''}});
});
// Once the input field is cleared all the available options will show up.
expect(res.queryByText('label1')).toBeTruthy();
expect(res.queryByText('label2')).toBeTruthy();
expect(res.queryByText('label3')).toBeTruthy();
const text4 = res.queryByText('label4');
expect(text4).toBeTruthy();
act(() => {
text4?.parentElement?.dispatchEvent(
new MouseEvent('click', {bubbles: true}),
);
});
expect(searchInput.value).toEqual('label4');
expect(onSelect).toBeCalledTimes(1);
// After onSelect the expanded menu gets closed.
expect(res.queryByText('label1')).toBeFalsy();
expect(res.queryByText('label2')).toBeFalsy();
expect(res.queryByText('label3')).toBeFalsy();
expect(res.queryByText('label4')).toBeFalsy();
});
test('Test the validation error.', async () => {
const handleNoResults = jest.fn();
const res = render(
<DropDownSearchView
list={[
{id: 'id1', label: 'label1 group'},
{id: 'id2', label: 'label2 group'},
{id: 'id3', label: 'label3 support'},
{id: 'id4', label: 'label4 support'},
]}
handleNoResults={handleNoResults}
selectedElementID={undefined}
/>,
);
const searchInput = (await res.findByTestId(
'search-input',
)) as HTMLInputElement;
expect(searchInput).toBeTruthy();
expect(searchInput.value).toEqual('');
searchInput.focus();
// Right now just the filtered elements will show up
expect(await res.findByText('label1 group')).toBeTruthy();
expect(await res.findByText('label2 group')).toBeTruthy();
expect(await res.findByText('label3 support')).toBeTruthy();
expect(await res.findByText('label4 support')).toBeTruthy();
act(() => {
fireEvent.change(searchInput, {target: {value: 'support'}});
});
// Only the items which satisfy the search query should be shown
expect(res.queryByText('label3 support')).toBeTruthy();
expect(res.queryByText('label4 support')).toBeTruthy();
expect(res.queryByText('label1 group')).toBeFalsy();
expect(res.queryByText('label2 group')).toBeFalsy();
act(() => {
fireEvent.change(searchInput, {target: {value: 'gibberish'}});
});
expect(handleNoResults).toBeCalled();
expect(res.queryByText('label3 support')).toBeFalsy();
expect(res.queryByText('label4 support')).toBeFalsy();
expect(res.queryByText('label1 group')).toBeFalsy();
expect(res.queryByText('label2 group')).toBeFalsy();
});