diff --git a/src/plugins/layout/layout2/Inspector.js b/src/plugins/layout/layout2/Inspector.js index c82d7f944..b08a9bfaa 100644 --- a/src/plugins/layout/layout2/Inspector.js +++ b/src/plugins/layout/layout2/Inspector.js @@ -5,7 +5,12 @@ * @format */ -import type {ElementID, Element, PluginClient} from 'flipper'; +import type { + ElementID, + Element, + PluginClient, + ElementSearchResultSet, +} from 'flipper'; import {ElementsInspector} from 'flipper'; import {Component} from 'react'; import debounce from 'lodash.debounce'; @@ -29,6 +34,7 @@ type Props = { onDataValueChanged: (path: Array, value: any) => void, setPersistedState: (state: $Shape) => void, persistedState: PersistedState, + searchResults: ?ElementSearchResultSet, }; export default class Inspector extends Component { diff --git a/src/plugins/layout/layout2/Search.js b/src/plugins/layout/layout2/Search.js new file mode 100644 index 000000000..8262f8c72 --- /dev/null +++ b/src/plugins/layout/layout2/Search.js @@ -0,0 +1,162 @@ +/** + * 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 + */ + +import type {PluginClient, ElementSearchResultSet} from 'flipper'; +import type {PersistedState} from './'; + +import { + SearchInput, + SearchBox, + SearchIcon, + LoadingIndicator, + styled, + colors, +} from 'flipper'; +import {Component} from 'react'; + +type SearchResultTree = {| + id: string, + isMatch: Boolean, + hasChildren: boolean, + children: ?Array, + element: Element, + axElement: Element, +|}; + +type Props = { + client: PluginClient, + inAXMode: boolean, + onSearchResults: (searchResults: ElementSearchResultSet) => void, + setPersistedState: (state: $Shape) => void, + persistedState: PersistedState, +}; + +type State = { + value: string, + outstandingSearchQuery: ?string, +}; + +const LoadingSpinner = styled(LoadingIndicator)({ + marginRight: 4, + marginLeft: 3, + marginTop: -1, +}); + +export default class Search extends Component { + state = { + value: '', + outstandingSearchQuery: null, + }; + + timer: TimeoutID; + + onChange = (e: SyntheticInputEvent<>) => { + clearTimeout(this.timer); + this.setState({ + value: e.target.value, + }); + this.timer = setTimeout(() => this.performSearch(e.target.value), 200); + }; + + onKeyDown = (e: SyntheticKeyboardEvent<>) => { + if (e.key === 'Enter') { + this.performSearch(this.state.value); + } + }; + + performSearch(query: string) { + this.setState({ + outstandingSearchQuery: query, + }); + + if (!query) { + this.displaySearchResults({query: '', results: null}); + } else { + this.props.client + .call('getSearchResults', {query, axEnabled: this.props.inAXMode}) + .then(response => this.displaySearchResults(response)); + } + } + + displaySearchResults({ + results, + query, + }: { + results: ?SearchResultTree, + query: string, + }) { + this.setState({ + outstandingSearchQuery: + query === this.state.outstandingSearchQuery + ? null + : this.state.outstandingSearchQuery, + }); + + const elements = this.getElementsFromSearchResultTree(results); + const expandedElements = elements.reduce( + (acc, {element}) => ({ + ...acc, + [element.id]: {...element, expanded: true}, + }), + {}, + ); + + this.props.setPersistedState({ + elements: { + ...this.props.persistedState.elements, + ...expandedElements, + }, + }); + + this.props.onSearchResults({ + matches: new Set(elements.filter(x => x.isMatch).map(x => x.element.id)), + query: query, + }); + } + + getElementsFromSearchResultTree( + tree: ?SearchResultTree, + ): Array { + if (!tree) { + return []; + } + let elements = [ + { + id: tree.id, + isMatch: tree.isMatch, + hasChildren: Boolean(tree.children), + element: tree.element, + axElement: tree.axElement, + }, + ]; + if (tree.children) { + for (const child of tree.children) { + elements = elements.concat(this.getElementsFromSearchResultTree(child)); + } + } + return elements; + } + + render() { + return ( + + + + {this.state.outstandingSearchQuery && } + + ); + } +} diff --git a/src/plugins/layout/layout2/index.js b/src/plugins/layout/layout2/index.js index e3d07e479..26eb20267 100644 --- a/src/plugins/layout/layout2/index.js +++ b/src/plugins/layout/layout2/index.js @@ -5,7 +5,7 @@ * @format */ -import type {ElementID, Element} from 'flipper'; +import type {ElementID, Element, ElementSearchResultSet} from 'flipper'; import { FlexColumn, @@ -20,6 +20,7 @@ import { import Inspector from './Inspector'; import ToolbarIcon from './ToolbarIcon'; import InspectorSidebar from './InspectorSidebar'; +import Search from './Search'; type State = {| init: boolean, @@ -28,6 +29,7 @@ type State = {| inAlignmentMode: boolean, selectedElement: ?ElementID, selectedAXElement: ?ElementID, + searchResults: ?ElementSearchResultSet, |}; export type PersistedState = {| @@ -52,6 +54,7 @@ export default class Layout extends FlipperPlugin { inAlignmentMode: false, selectedElement: null, selectedAXElement: null, + searchResults: null, }; componentDidMount() { @@ -116,6 +119,7 @@ export default class Layout extends FlipperPlugin { setPersistedState: this.props.setPersistedState, persistedState: this.props.persistedState, onDataValueChanged: this.onDataValueChanged, + searchResults: this.state.searchResults, }; let element; @@ -152,6 +156,15 @@ export default class Layout extends FlipperPlugin { icon="borders" active={this.state.inAlignmentMode} /> + + this.setState({searchResults}) + } + inAXMode={this.state.inAXMode} + />