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:
committed by
Facebook GitHub Bot
parent
281cd67ddb
commit
38186c8995
221
desktop/app/src/chrome/DropDownSearchView.tsx
Normal file
221
desktop/app/src/chrome/DropDownSearchView.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
desktop/app/src/ui/components/Line.tsx
Normal file
20
desktop/app/src/ui/components/Line.tsx
Normal 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;
|
||||||
@@ -30,15 +30,21 @@ const SearchBar = styled(Toolbar)({
|
|||||||
});
|
});
|
||||||
SearchBar.displayName = 'Searchable:SearchBar';
|
SearchBar.displayName = 'Searchable:SearchBar';
|
||||||
|
|
||||||
export const SearchBox = styled(FlexBox)({
|
export const SearchBox = styled(FlexBox)<{isInvalidInput?: boolean}>(
|
||||||
backgroundColor: colors.white,
|
(props) => {
|
||||||
borderRadius: '999em',
|
return {
|
||||||
border: `1px solid ${colors.light15}`,
|
backgroundColor: colors.white,
|
||||||
height: '100%',
|
borderRadius: '999em',
|
||||||
width: '100%',
|
border: `1px solid ${
|
||||||
alignItems: 'center',
|
!props.isInvalidInput ? colors.light15 : colors.red
|
||||||
paddingLeft: 4,
|
}`,
|
||||||
});
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: 4,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
SearchBox.displayName = 'Searchable:SearchBox';
|
SearchBox.displayName = 'Searchable:SearchBox';
|
||||||
|
|
||||||
export const SearchInput = styled(Input)<{
|
export const SearchInput = styled(Input)<{
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export {default as Tooltip} from './components/Tooltip';
|
|||||||
export {default as TooltipProvider} from './components/TooltipProvider';
|
export {default as TooltipProvider} from './components/TooltipProvider';
|
||||||
export {default as ResizeSensor} from './components/ResizeSensor';
|
export {default as ResizeSensor} from './components/ResizeSensor';
|
||||||
export {default as StatusIndicator} from './components/StatusIndicator';
|
export {default as StatusIndicator} from './components/StatusIndicator';
|
||||||
|
export {default as Line} from './components/Line';
|
||||||
// typography
|
// typography
|
||||||
export {default as HorizontalRule} from './components/HorizontalRule';
|
export {default as HorizontalRule} from './components/HorizontalRule';
|
||||||
export {default as VerticalRule} from './components/VerticalRule';
|
export {default as VerticalRule} from './components/VerticalRule';
|
||||||
|
|||||||
@@ -147,7 +147,8 @@
|
|||||||
],
|
],
|
||||||
"magnifying-glass": [
|
"magnifying-glass": [
|
||||||
16,
|
16,
|
||||||
20
|
20,
|
||||||
|
24
|
||||||
],
|
],
|
||||||
"messages": [
|
"messages": [
|
||||||
12
|
12
|
||||||
|
|||||||
Reference in New Issue
Block a user