Add dropdown for selecting groups

Summary: This diff refactors the group selection to the dropdown. As with the growing list of grps, dropdown will be easy to search and scale

Reviewed By: mweststrate

Differential Revision: D21175998

fbshipit-source-id: 90f1a81dfc6c2232cd2dcf767ed01205fc63e1fd
This commit is contained in:
Pritesh Nandgaonkar
2020-04-29 05:39:17 -07:00
committed by Facebook GitHub Bot
parent 281cd67ddb
commit 38186c8995
5 changed files with 259 additions and 11 deletions

View File

@@ -0,0 +1,221 @@
/**
* 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
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 initialElement = list.find((e) => {
return e.id === selectedElementID;
});
const [filteredElements, setFilteredElements] = useState<Array<Element>>([]);
const [searchedValue, setSearchedValue] = useState<string>(
initialElement ? initialElement.label : '',
);
const [selectedElement, setSelectedElement] = useState<Element | undefined>(
initialElement,
);
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],
);
// 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}
/>
</SearchBox>
</Tooltip>
{filteredElements.length > 0 && focussed && (
<OverlayContainer>
<ListViewContainer scrollable={true}>
{filteredElements.map((e, idx) => {
return (
<>
<RowComponent
elem={e}
onClick={onSelectCallBack}
selected={selectedElement && e.id == selectedElement.id}
/>
{idx < filteredElements.length - 1 && <Separator />}
</>
);
})}
</ListViewContainer>
</OverlayContainer>
)}
</FlexColumn>
);
}

View File

@@ -0,0 +1,20 @@
/**
* 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 styled from '@emotion/styled';
import View from './View';
import {colors} from './colors';
const Line = styled(View)<{color?: string}>(({color}) => ({
backgroundColor: color ? color : colors.greyTint2,
height: 1,
width: 'auto',
flexShrink: 0,
}));
export default Line;

View File

@@ -30,15 +30,21 @@ const SearchBar = styled(Toolbar)({
});
SearchBar.displayName = 'Searchable:SearchBar';
export const SearchBox = styled(FlexBox)({
backgroundColor: colors.white,
borderRadius: '999em',
border: `1px solid ${colors.light15}`,
height: '100%',
width: '100%',
alignItems: 'center',
paddingLeft: 4,
});
export const SearchBox = styled(FlexBox)<{isInvalidInput?: boolean}>(
(props) => {
return {
backgroundColor: colors.white,
borderRadius: '999em',
border: `1px solid ${
!props.isInvalidInput ? colors.light15 : colors.red
}`,
height: '100%',
width: '100%',
alignItems: 'center',
paddingLeft: 4,
};
},
);
SearchBox.displayName = 'Searchable:SearchBox';
export const SearchInput = styled(Input)<{

View File

@@ -121,7 +121,7 @@ export {default as Tooltip} from './components/Tooltip';
export {default as TooltipProvider} from './components/TooltipProvider';
export {default as ResizeSensor} from './components/ResizeSensor';
export {default as StatusIndicator} from './components/StatusIndicator';
export {default as Line} from './components/Line';
// typography
export {default as HorizontalRule} from './components/HorizontalRule';
export {default as VerticalRule} from './components/VerticalRule';

View File

@@ -147,7 +147,8 @@
],
"magnifying-glass": [
16,
20
20,
24
],
"messages": [
12