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';
|
||||
|
||||
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)<{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
],
|
||||
"magnifying-glass": [
|
||||
16,
|
||||
20
|
||||
20,
|
||||
24
|
||||
],
|
||||
"messages": [
|
||||
12
|
||||
|
||||
Reference in New Issue
Block a user