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
222 lines
5.7 KiB
TypeScript
222 lines
5.7 KiB
TypeScript
/**
|
|
* 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>
|
|
);
|
|
}
|