diff --git a/desktop/app/src/ui/components/elements-inspector/elements.tsx b/desktop/app/src/ui/components/elements-inspector/elements.tsx index 2fb5316e7..d392a7fd9 100644 --- a/desktop/app/src/ui/components/elements-inspector/elements.tsx +++ b/desktop/app/src/ui/components/elements-inspector/elements.tsx @@ -20,7 +20,7 @@ import styled from '@emotion/styled'; import {clipboard, MenuItemConstructorOptions} from 'electron'; import React, {MouseEvent, KeyboardEvent} from 'react'; -const ROW_HEIGHT = 23; +export const ROW_HEIGHT = 23; const backgroundColor = (props: { selected: boolean; diff --git a/desktop/plugins/layout/Inspector.tsx b/desktop/plugins/layout/Inspector.tsx index d71664025..f2a52a1f3 100644 --- a/desktop/plugins/layout/Inspector.tsx +++ b/desktop/plugins/layout/Inspector.tsx @@ -13,11 +13,19 @@ import { PluginClient, ElementsInspector, ElementSearchResultSet, + FlexColumn, + styled, } from 'flipper'; -import {Component} from 'react'; import {debounce} from 'lodash'; +import {Component} from 'react'; import {PersistedState, ElementMap} from './'; import React from 'react'; +import MultipleSelectorSection from './MultipleSelectionSection'; + +const ElementsInspectorContainer = styled(FlexColumn)({ + width: '100%', + justifyContent: 'space-between', +}); type GetNodesOptions = { force?: boolean; @@ -25,7 +33,12 @@ type GetNodesOptions = { forAccessibilityEvent?: boolean; }; -type ElementSelectorNode = {[id: string]: ElementSelectorNode}; +export type ElementSelectorNode = {[id: string]: ElementSelectorNode}; +export type ElementSelectorData = { + leaves: Array; + tree: ElementSelectorNode; + elements: ElementMap; +}; type Props = { ax?: boolean; @@ -41,7 +54,14 @@ type Props = { searchResults: ElementSearchResultSet | null; }; -export default class Inspector extends Component { +type State = { + elementSelector: ElementSelectorData | null; + axElementSelector: ElementSelectorData | null; +}; + +export default class Inspector extends Component { + state: State = {elementSelector: null, axElementSelector: null}; + call() { return { GET_ROOT: this.props.ax ? 'getAXRoot' : 'getRoot', @@ -133,12 +153,29 @@ export default class Inspector extends Component { this.props.client.subscribe( this.call().SELECT, - ({path, tree}: {path?: Array; tree?: ElementSelectorNode}) => { - if (tree) { - this._getAndExpandPathFromTree(tree); - } else if (path) { + async ({ + path, + tree, + }: { + path?: Array; + tree?: ElementSelectorNode; + }) => { + if (path) { this.getAndExpandPath(path); } + if (tree) { + const leaves = this.getElementLeaves(tree); + const elementArray = await this.getNodes(leaves, {}); + const elements = leaves.reduce( + (acc, cur, idx) => ({...acc, [cur]: elementArray[idx]}), + {}, + ); + if (this.props.ax) { + this.setState({axElementSelector: {tree, leaves, elements}}); + } else { + this.setState({elementSelector: {tree, leaves, elements}}); + } + } }, ); @@ -373,19 +410,33 @@ export default class Inspector extends Component { }; render() { + const selectorData = this.props.ax + ? this.state.axElementSelector + : this.state.elementSelector; + return this.root() ? ( - + + + {selectorData && selectorData.leaves.length > 1 ? ( + + ) : null} + ) : null; } } diff --git a/desktop/plugins/layout/MultipleSelectionSection.tsx b/desktop/plugins/layout/MultipleSelectionSection.tsx new file mode 100644 index 000000000..214cbb8f1 --- /dev/null +++ b/desktop/plugins/layout/MultipleSelectionSection.tsx @@ -0,0 +1,84 @@ +/** + * 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 { + FlexColumn, + FlexBox, + Element, + ElementID, + ElementsInspector, + colors, + styled, +} from 'flipper'; +import React, {memo, useState} from 'react'; +import {ROW_HEIGHT} from '../../app/src/ui/components/elements-inspector/elements'; + +const MultipleSelectorSectionContainer = styled(FlexColumn)({ + maxHeight: 3 * ROW_HEIGHT + 24, +}); + +const MultipleSelectorSectionTitle = styled(FlexBox)({ + cursor: 'pointer', + backgroundColor: '#f6f7f9', + padding: '2px', + paddingLeft: '9px', + width: '325px', + height: '20px', + fontWeight: 500, + boxShadow: '2px 2px 2px #ccc', + border: `1px solid ${colors.light20}`, + borderTopLeftRadius: '4px', + borderTopRightRadius: '4px', + textAlign: 'center', +}); + +type MultipleSelectorSectionProps = { + initialSelectedElement: ElementID | null | undefined; + elements: {[id: string]: Element}; + onElementSelected: (key: string) => void; + onElementHovered: + | ((key: string | null | undefined) => any) + | null + | undefined; +}; + +const MultipleSelectorSection: React.FC = memo( + (props: MultipleSelectorSectionProps) => { + const { + initialSelectedElement, + elements, + onElementSelected, + onElementHovered, + } = props; + const [selectedId, setSelectedId] = useState( + initialSelectedElement, + ); + return ( + + + Multiple elements found at the target coordinates + + { + setSelectedId(key); + onElementSelected(key); + }} + onElementHovered={onElementHovered} + onElementExpanded={() => {}} + onValueChanged={null} + root={null} + selected={selectedId} + elements={elements} + /> + + ); + }, +); + +export default MultipleSelectorSection;