diff --git a/desktop/app/src/chrome/DropDownSearchView.tsx b/desktop/app/src/chrome/DropDownSearchView.tsx new file mode 100644 index 000000000..1c505834b --- /dev/null +++ b/desktop/app/src/chrome/DropDownSearchView.tsx @@ -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; + onSelect?: (id: string, label: string) => void; + handleNoResults?: (value: string) => void; + selectedElementID?: string; +}; + +function RowComponent(props: { + elem: Element; + onClick: (id: string) => void; + selected?: boolean; +}) { + return ( + { + props.onClick(props.elem.id); + }}> + {props.elem.label} + + {props.selected && ( + + )} + + ); +} + +export default function (props: Props) { + const {list, handleNoResults, onSelect, selectedElementID} = props; + const initialElement = list.find((e) => { + return e.id === selectedElementID; + }); + const [filteredElements, setFilteredElements] = useState>([]); + const [searchedValue, setSearchedValue] = useState( + initialElement ? initialElement.label : '', + ); + const [selectedElement, setSelectedElement] = useState( + initialElement, + ); + const [focussed, setFocus] = useState(false); + const wrapperRef = useRef(null); + + const onChangeCallBack = useCallback( + (e: React.ChangeEvent) => { + 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) => { + 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 ( + + + 0}> + + + + + {filteredElements.length > 0 && focussed && ( + + + {filteredElements.map((e, idx) => { + return ( + <> + + {idx < filteredElements.length - 1 && } + + ); + })} + + + )} + + ); +} diff --git a/desktop/app/src/ui/components/Line.tsx b/desktop/app/src/ui/components/Line.tsx new file mode 100644 index 000000000..35600387f --- /dev/null +++ b/desktop/app/src/ui/components/Line.tsx @@ -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; diff --git a/desktop/app/src/ui/components/searchable/Searchable.tsx b/desktop/app/src/ui/components/searchable/Searchable.tsx index e38565a92..a9266875b 100644 --- a/desktop/app/src/ui/components/searchable/Searchable.tsx +++ b/desktop/app/src/ui/components/searchable/Searchable.tsx @@ -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)<{ diff --git a/desktop/app/src/ui/index.tsx b/desktop/app/src/ui/index.tsx index 80b160fc1..b559f5751 100644 --- a/desktop/app/src/ui/index.tsx +++ b/desktop/app/src/ui/index.tsx @@ -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'; diff --git a/desktop/static/icons.json b/desktop/static/icons.json index a839156ad..0631248b5 100644 --- a/desktop/static/icons.json +++ b/desktop/static/icons.json @@ -147,7 +147,8 @@ ], "magnifying-glass": [ 16, - 20 + 20, + 24 ], "messages": [ 12