diff --git a/src/plugins/navigation/__tests__/testNavigationPlugin.node.js b/src/plugins/navigation/__tests__/testNavigationPlugin.node.js index 18db6fb6f..f64fe3458 100644 --- a/src/plugins/navigation/__tests__/testNavigationPlugin.node.js +++ b/src/plugins/navigation/__tests__/testNavigationPlugin.node.js @@ -7,17 +7,22 @@ */ import NavigationPlugin from '../'; +import {DefaultProvider} from '../util/autoCompleteProvider'; -import type {PersistedState} from '../flow-types'; +import type {Bookmark, PersistedState, URI} from '../flow-types'; function constructPersistedStateMock(): PersistedState { return { + bookmarksProvider: new DefaultProvider(), + bookmarks: new Map(), navigationEvents: [], }; } function constructPersistedStateMockWithEvents(): PersistedState { return { + bookmarksProvider: new DefaultProvider(), + bookmarks: new Map(), navigationEvents: [ { uri: 'mock://this_is_a_mock_uri/mock/1', diff --git a/src/plugins/navigation/components/AutoCompleteSheet.js b/src/plugins/navigation/components/AutoCompleteSheet.js index 5625b6241..b2deba7ae 100644 --- a/src/plugins/navigation/components/AutoCompleteSheet.js +++ b/src/plugins/navigation/components/AutoCompleteSheet.js @@ -6,22 +6,23 @@ * @flow strict-local */ -import {styled} from 'flipper'; -import {useEffect, useState} from 'react'; +import {Glyph, styled} from 'flipper'; +import {useItemNavigation} from '../hooks/autoCompleteSheet'; +import {filterProvidersToLineItems} from '../util/autoCompleteProvider'; -import type {Bookmark} from '../flow-types'; +import type {AutoCompleteProvider} from '../flow-types'; type Props = {| - bookmarks: Map, + providers: Array, onHighlighted: string => void, onNavigate: string => void, + query: string, |}; const MAX_ITEMS = 5; const AutoCompleteSheetContainer = styled('div')({ width: '100%', - overflowY: 'scroll', position: 'absolute', top: '100%', backgroundColor: 'white', @@ -44,56 +45,26 @@ const SheetItem = styled('div')({ }, }); -// Menu Item Navigation Hook -const useItemNavigation = ( - bookmarks: Array, - onHighlighted: string => void, -) => { - const [selectedItem, setSelectedItem] = useState(-1); - - const handleKeyPress = ({key}) => { - switch (key) { - case 'ArrowDown': { - const newSelectedItem = - selectedItem < MAX_ITEMS - 1 ? selectedItem + 1 : selectedItem; - setSelectedItem(newSelectedItem); - onHighlighted(bookmarks[newSelectedItem].uri); - break; - } - case 'ArrowUp': { - const newSelectedItem = - selectedItem > 0 ? selectedItem - 1 : selectedItem; - setSelectedItem(newSelectedItem); - onHighlighted(bookmarks[newSelectedItem].uri); - break; - } - default: - break; - } - }; - - useEffect(() => { - window.addEventListener('keydown', handleKeyPress); - return () => { - window.removeEventListener('keydown', handleKeyPress); - }; - }); - - return selectedItem; -}; +const SheetItemIcon = styled('span')({ + padding: 8, +}); export default (props: Props) => { - const {bookmarks, onHighlighted, onNavigate} = props; - const filteredBookmarks = [...bookmarks.values()].slice(0, MAX_ITEMS); - const selectedItem = useItemNavigation(filteredBookmarks, onHighlighted); + const {providers, onHighlighted, onNavigate, query} = props; + const lineItems = filterProvidersToLineItems(providers, query, MAX_ITEMS); + lineItems.unshift({uri: query, matchPattern: query, icon: 'send'}); + const selectedItem = useItemNavigation(lineItems, onHighlighted); return ( - {filteredBookmarks.map((bookmark, idx) => ( + {lineItems.map((lineItem, idx) => ( onNavigate(bookmark.uri)}> - {bookmark.uri} + key={idx} + onMouseDown={() => onNavigate(lineItem.uri)}> + + + + {lineItem.matchPattern} ))} diff --git a/src/plugins/navigation/components/SearchBar.js b/src/plugins/navigation/components/SearchBar.js index b36247e7a..9f3c89301 100644 --- a/src/plugins/navigation/components/SearchBar.js +++ b/src/plugins/navigation/components/SearchBar.js @@ -16,18 +16,20 @@ import { } from 'flipper'; import {AutoCompleteSheet, IconButton, FavoriteButton} from './'; -import type {Bookmark} from '../flow-types'; +import type {AutoCompleteProvider, Bookmark} from '../flow-types'; type Props = {| onFavorite: (query: string) => void, onNavigate: (query: string) => void, bookmarks: Map, + providers: Array, |}; type State = {| query: string, inputFocused: boolean, autoCompleteSheetOpen: boolean, + searchInputValue: string, |}; const IconContainer = styled('div')({ @@ -65,73 +67,84 @@ class SearchBar extends Component { inputFocused: false, autoCompleteSheetOpen: false, query: '', + searchInputValue: '', }; - favorite = (query: string) => { - this.props.onFavorite(query); + favorite = (searchInputValue: string) => { + this.props.onFavorite(searchInputValue); }; - navigateTo = (query: string) => { - this.setState({query}); - this.props.onNavigate(query); + navigateTo = (searchInputValue: string) => { + this.setState({query: searchInputValue, searchInputValue}); + this.props.onNavigate(searchInputValue); }; queryInputChanged = (event: SyntheticInputEvent<>) => { - this.setState({query: event.target.value}); + const value = event.target.value; + this.setState({query: value, searchInputValue: value}); }; render = () => { - const {bookmarks} = this.props; - const {autoCompleteSheetOpen, inputFocused, query} = this.state; + const {bookmarks, providers} = this.props; + const { + autoCompleteSheetOpen, + inputFocused, + searchInputValue, + query, + } = this.state; return ( this.setState({ autoCompleteSheetOpen: false, inputFocused: false, }) } - onFocus={() => + onFocus={event => { + event.target.select(); this.setState({ autoCompleteSheetOpen: true, inputFocused: true, - }) - } + }); + }} onChange={this.queryInputChanged} onKeyPress={e => { if (e.key === 'Enter') { - this.navigateTo(this.state.query); + this.navigateTo(this.state.searchInputValue); e.target.blur(); } }} placeholder="Navigate To..." /> - {autoCompleteSheetOpen ? ( + {autoCompleteSheetOpen && query.length > 0 ? ( this.setState({query: newQuery})} + onHighlighted={newInputValue => + this.setState({searchInputValue: newInputValue}) + } + query={query} /> ) : null} - {query.length > 0 ? ( + {searchInputValue.length > 0 ? ( this.navigateTo(this.state.query)} + onClick={() => this.navigateTo(searchInputValue)} /> this.favorite(this.state.query)} + highlighted={bookmarks.has(searchInputValue)} + onClick={() => this.favorite(searchInputValue)} /> ) : null} diff --git a/src/plugins/navigation/flow-types.js b/src/plugins/navigation/flow-types.js index f926eb65a..9ee06d539 100644 --- a/src/plugins/navigation/flow-types.js +++ b/src/plugins/navigation/flow-types.js @@ -9,13 +9,14 @@ export type URI = string; export type State = {| - bookmarks: Map, shouldShowSaveBookmarkDialog: boolean, saveBookmarkURI: ?URI, |}; export type PersistedState = {| + bookmarks: Map, navigationEvents: Array, + bookmarksProvider: AutoCompleteProvider, |}; export type NavigationEvent = {| diff --git a/src/plugins/navigation/hooks/autoCompleteSheet.js b/src/plugins/navigation/hooks/autoCompleteSheet.js new file mode 100644 index 000000000..8e6299893 --- /dev/null +++ b/src/plugins/navigation/hooks/autoCompleteSheet.js @@ -0,0 +1,51 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + * @flow strict-local + */ + +import {useEffect, useState} from 'react'; +import type {AutoCompleteLineItem} from '../flow-types'; + +export const useItemNavigation = ( + lineItems: Array, + onHighlighted: string => void, +) => { + const [selectedItem, setSelectedItem] = useState(0); + + const handleKeyPress = ({key}) => { + switch (key) { + case 'ArrowDown': { + const newSelectedItem = + selectedItem < lineItems.length - 1 + ? selectedItem + 1 + : lineItems.length - 1; + setSelectedItem(newSelectedItem); + onHighlighted(lineItems[newSelectedItem].uri); + break; + } + case 'ArrowUp': { + const newSelectedItem = + selectedItem > 0 ? selectedItem - 1 : selectedItem; + setSelectedItem(newSelectedItem); + onHighlighted(lineItems[newSelectedItem].uri); + break; + } + default: { + setSelectedItem(0); + break; + } + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleKeyPress); + return () => { + window.removeEventListener('keydown', handleKeyPress); + }; + }); + + return selectedItem; +}; diff --git a/src/plugins/navigation/index.js b/src/plugins/navigation/index.js index 723bf19b8..56ad37862 100644 --- a/src/plugins/navigation/index.js +++ b/src/plugins/navigation/index.js @@ -19,6 +19,10 @@ import { readBookmarksFromDB, writeBookmarkToDB, } from './util/indexedDB'; +import { + bookmarksToAutoCompleteProvider, + DefaultProvider, +} from './util/autoCompleteProvider'; import type { State, @@ -35,10 +39,11 @@ export default class extends FlipperPlugin { static defaultPersistedState: PersistedState = { navigationEvents: [], + bookmarks: new Map(), + bookmarksProvider: new DefaultProvider(), }; state = { - bookmarks: new Map(), shouldShowSaveBookmarkDialog: false, saveBookmarkURI: null, }; @@ -69,7 +74,10 @@ export default class extends FlipperPlugin { componentDidMount = () => { readBookmarksFromDB().then(bookmarks => { - this.setState({bookmarks}); + this.props.setPersistedState({ + bookmarks: bookmarks, + bookmarksProvider: bookmarksToAutoCompleteProvider(bookmarks), + }); }); }; @@ -95,29 +103,38 @@ export default class extends FlipperPlugin { commonName: bookmark.commonName.length > 0 ? bookmark.commonName : bookmark.uri, }; + writeBookmarkToDB(newBookmark); - const newMapRef = this.state.bookmarks; + const newMapRef = this.props.persistedState.bookmarks; newMapRef.set(newBookmark.uri, newBookmark); - this.setState({bookmarks: newMapRef}); + this.props.setPersistedState({ + bookmarks: newMapRef, + bookmarksProvider: bookmarksToAutoCompleteProvider(newMapRef), + }); }; removeBookmark = (uri: string) => { removeBookmark(uri); - const newMapRef = this.state.bookmarks; + const newMapRef = this.props.persistedState.bookmarks; newMapRef.delete(uri); - this.setState({bookmarks: newMapRef}); + this.props.setPersistedState({ + bookmarks: newMapRef, + bookmarksProvider: bookmarksToAutoCompleteProvider(newMapRef), + }); }; render() { + const {saveBookmarkURI, shouldShowSaveBookmarkDialog} = this.state; const { bookmarks, - saveBookmarkURI, - shouldShowSaveBookmarkDialog, - } = this.state; - const {navigationEvents} = this.props.persistedState; + bookmarksProvider, + navigationEvents, + } = this.props.persistedState; + const autoCompleteProviders = [bookmarksProvider]; return ( (); + return this; +} + export const bookmarksToAutoCompleteProvider: ( Map, ) => AutoCompleteProvider = bookmarks => {