Improve multiple element selector UI
Summary: Layer selection is pretty easy to miss, as reported in for example: https://fb.workplace.com/groups/flippersupport/permalink/1098169193997071/ Moved the layer selection to the top of the view and gave it some highlighting + dynamic height. The section is no longer collapsible. Changelog: [Layout] Make the layer selection more prominent Reviewed By: priteshrnandgaonkar Differential Revision: D27708650 fbshipit-source-id: c86a55c3a20794aee86e64b6766b2ca4dd6b563f
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5db2ef1275
commit
0fe879c838
@@ -10,6 +10,7 @@
|
|||||||
import {Component} from 'react';
|
import {Component} from 'react';
|
||||||
import {Elements, DecorateRow, ContextMenuExtension} from './elements';
|
import {Elements, DecorateRow, ContextMenuExtension} from './elements';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {Layout} from '../Layout';
|
||||||
|
|
||||||
export type ElementID = string;
|
export type ElementID = string;
|
||||||
|
|
||||||
@@ -72,11 +73,16 @@ export type ElementsInspectorProps = {
|
|||||||
alternateRowColor?: boolean;
|
alternateRowColor?: boolean;
|
||||||
contextMenuExtensions?: () => Array<ContextMenuExtension>;
|
contextMenuExtensions?: () => Array<ContextMenuExtension>;
|
||||||
decorateRow?: DecorateRow;
|
decorateRow?: DecorateRow;
|
||||||
|
/**
|
||||||
|
* By default the ElementsInspector takes all available space and is scrollab.e
|
||||||
|
*/
|
||||||
|
scrollable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ElementsInspector extends Component<ElementsInspectorProps> {
|
export class ElementsInspector extends Component<ElementsInspectorProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
alternateRowColor: true,
|
alternateRowColor: true,
|
||||||
|
scrollable: true,
|
||||||
};
|
};
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@@ -91,9 +97,10 @@ export class ElementsInspector extends Component<ElementsInspectorProps> {
|
|||||||
alternateRowColor,
|
alternateRowColor,
|
||||||
contextMenuExtensions,
|
contextMenuExtensions,
|
||||||
decorateRow,
|
decorateRow,
|
||||||
|
scrollable,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
const renderedElems = (
|
||||||
<Elements
|
<Elements
|
||||||
onElementExpanded={onElementExpanded}
|
onElementExpanded={onElementExpanded}
|
||||||
onElementSelected={onElementSelected}
|
onElementSelected={onElementSelected}
|
||||||
@@ -108,5 +115,10 @@ export class ElementsInspector extends Component<ElementsInspectorProps> {
|
|||||||
decorateRow={decorateRow}
|
decorateRow={decorateRow}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
if (scrollable) {
|
||||||
|
return <Layout.ScrollContainer>{renderedElems}</Layout.ScrollContainer>;
|
||||||
|
} else {
|
||||||
|
return renderedElems;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -449,8 +449,6 @@ function containsKeyInSearchResults(
|
|||||||
const ElementsContainer = styled('div')({
|
const ElementsContainer = styled('div')({
|
||||||
display: 'table',
|
display: 'table',
|
||||||
backgroundColor: theme.backgroundDefault,
|
backgroundColor: theme.backgroundDefault,
|
||||||
minHeight: '100%',
|
|
||||||
minWidth: '100%',
|
|
||||||
});
|
});
|
||||||
ElementsContainer.displayName = 'Elements:ElementsContainer';
|
ElementsContainer.displayName = 'Elements:ElementsContainer';
|
||||||
|
|
||||||
@@ -489,7 +487,6 @@ export class Elements extends PureComponent<ElementsProps, ElementsState> {
|
|||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
alternateRowColor: true,
|
alternateRowColor: true,
|
||||||
};
|
};
|
||||||
_outerRef = React.createRef<HTMLDivElement>();
|
|
||||||
constructor(props: ElementsProps, context: Object) {
|
constructor(props: ElementsProps, context: Object) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -690,24 +687,14 @@ export class Elements extends PureComponent<ElementsProps, ElementsState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
scrollToSelectionRefHandler = (selectedRow: HTMLDivElement | null) => {
|
scrollToSelectionRefHandler = (selectedRow: HTMLDivElement | null) => {
|
||||||
if (
|
if (selectedRow && this.state.scrolledElement !== this.props.selected) {
|
||||||
!selectedRow ||
|
// second child is the element containing the element name
|
||||||
!this._outerRef.current ||
|
// by scrolling to the second element, we make sure padding is addressed and we scroll horizontally as well
|
||||||
this.props.selected === this.state.scrolledElement
|
selectedRow?.children[1]?.scrollIntoView?.({
|
||||||
) {
|
block: 'center',
|
||||||
return;
|
inline: 'center',
|
||||||
}
|
|
||||||
this.setState({scrolledElement: this.props.selected});
|
|
||||||
const outer = this._outerRef.current;
|
|
||||||
if (outer.scrollTo) {
|
|
||||||
outer.scrollTo({
|
|
||||||
top: this._calculateScrollTop(
|
|
||||||
outer.offsetHeight,
|
|
||||||
outer.scrollTop,
|
|
||||||
selectedRow.offsetHeight,
|
|
||||||
selectedRow.offsetTop,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
this.setState({scrolledElement: this.props.selected});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -761,7 +748,7 @@ export class Elements extends PureComponent<ElementsProps, ElementsState> {
|
|||||||
contextMenuExtensions={contextMenuExtensions}
|
contextMenuExtensions={contextMenuExtensions}
|
||||||
decorateRow={decorateRow}
|
decorateRow={decorateRow}
|
||||||
forwardedRef={
|
forwardedRef={
|
||||||
selected == row.key && this.state.scrolledElement !== selected
|
selected == row.key // && this.state.scrolledElement !== selected
|
||||||
? this.scrollToSelectionRefHandler
|
? this.scrollToSelectionRefHandler
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
@@ -771,11 +758,9 @@ export class Elements extends PureComponent<ElementsProps, ElementsState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Layout.ScrollContainer ref={this._outerRef}>
|
<ElementsContainer onKeyDown={this.onKeyDown} tabIndex={0}>
|
||||||
<ElementsContainer onKeyDown={this.onKeyDown} tabIndex={0}>
|
{this.state.flatElements.map(this.buildRow)}
|
||||||
{this.state.flatElements.map(this.buildRow)}
|
</ElementsContainer>
|
||||||
</ElementsContainer>
|
|
||||||
</Layout.ScrollContainer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,19 +13,13 @@ import {
|
|||||||
PluginClient,
|
PluginClient,
|
||||||
ElementsInspector,
|
ElementsInspector,
|
||||||
ElementSearchResultSet,
|
ElementSearchResultSet,
|
||||||
FlexColumn,
|
|
||||||
styled,
|
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import {debounce} from 'lodash';
|
import {debounce} from 'lodash';
|
||||||
import {Component} from 'react';
|
import {Component} from 'react';
|
||||||
import {PersistedState, ElementMap} from './';
|
import {PersistedState, ElementMap} from './';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MultipleSelectorSection from './MultipleSelectionSection';
|
import MultipleSelectorSection from './MultipleSelectionSection';
|
||||||
|
import {Layout} from 'flipper-plugin';
|
||||||
const ElementsInspectorContainer = styled(FlexColumn)({
|
|
||||||
width: '100%',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
});
|
|
||||||
|
|
||||||
type GetNodesOptions = {
|
type GetNodesOptions = {
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
@@ -448,7 +442,17 @@ export default class Inspector extends Component<Props, State> {
|
|||||||
: this.state.elementSelector;
|
: this.state.elementSelector;
|
||||||
|
|
||||||
return this.root() ? (
|
return this.root() ? (
|
||||||
<ElementsInspectorContainer>
|
<Layout.Top>
|
||||||
|
{selectorData && selectorData.leaves.length > 1 ? (
|
||||||
|
<MultipleSelectorSection
|
||||||
|
initialSelectedElement={this.selected()}
|
||||||
|
elements={selectorData.elements}
|
||||||
|
onElementSelected={this.onElementSelectedAndExpanded}
|
||||||
|
onElementHovered={this.onElementHovered}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
<ElementsInspector
|
<ElementsInspector
|
||||||
onElementSelected={this.onElementSelectedAtMainSection}
|
onElementSelected={this.onElementSelectedAtMainSection}
|
||||||
onElementHovered={this.onElementHovered}
|
onElementHovered={this.onElementHovered}
|
||||||
@@ -460,15 +464,7 @@ export default class Inspector extends Component<Props, State> {
|
|||||||
focused={this.focused()}
|
focused={this.focused()}
|
||||||
contextMenuExtensions={this.getAXContextMenuExtensions}
|
contextMenuExtensions={this.getAXContextMenuExtensions}
|
||||||
/>
|
/>
|
||||||
{selectorData && selectorData.leaves.length > 1 ? (
|
</Layout.Top>
|
||||||
<MultipleSelectorSection
|
|
||||||
initialSelectedElement={this.selected()}
|
|
||||||
elements={selectorData.elements}
|
|
||||||
onElementSelected={this.onElementSelectedAndExpanded}
|
|
||||||
onElementHovered={this.onElementHovered}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</ElementsInspectorContainer>
|
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,41 +7,16 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {Typography} from 'antd';
|
||||||
FlexColumn,
|
import {Element, ElementID, ElementsInspector, styled} from 'flipper';
|
||||||
FlexBox,
|
import {Layout, theme} from 'flipper-plugin';
|
||||||
Element,
|
|
||||||
ElementID,
|
|
||||||
ElementsInspector,
|
|
||||||
Glyph,
|
|
||||||
colors,
|
|
||||||
styled,
|
|
||||||
} from 'flipper';
|
|
||||||
import React, {memo, useState} from 'react';
|
import React, {memo, useState} from 'react';
|
||||||
|
|
||||||
const MultipleSelectorSectionContainer = styled(FlexColumn)({
|
const MultipleSelectorSectionContainer = styled(Layout.Container)({
|
||||||
maxHeight: 100,
|
padding: theme.space.medium,
|
||||||
});
|
background: theme.backgroundWash,
|
||||||
|
borderTop: `${theme.space.tiny}px solid ${theme.warningColor}`,
|
||||||
const MultipleSelectorSectionTitle = styled(FlexBox)({
|
borderBottom: `${theme.space.tiny}px solid ${theme.warningColor}`,
|
||||||
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',
|
|
||||||
});
|
|
||||||
|
|
||||||
const Chevron = styled(Glyph)({
|
|
||||||
marginRight: 4,
|
|
||||||
marginLeft: -2,
|
|
||||||
marginBottom: 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type MultipleSelectorSectionProps = {
|
type MultipleSelectorSectionProps = {
|
||||||
@@ -65,29 +40,24 @@ const MultipleSelectorSection: React.FC<MultipleSelectorSectionProps> = memo(
|
|||||||
const [selectedId, setSelectedId] = useState<ElementID | null | undefined>(
|
const [selectedId, setSelectedId] = useState<ElementID | null | undefined>(
|
||||||
initialSelectedElement,
|
initialSelectedElement,
|
||||||
);
|
);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
return (
|
return (
|
||||||
<MultipleSelectorSectionContainer>
|
<MultipleSelectorSectionContainer gap>
|
||||||
<MultipleSelectorSectionTitle
|
<Typography.Text>
|
||||||
onClick={() => {
|
Multiple elements were found at the target coordinates. Please select
|
||||||
setCollapsed(!collapsed);
|
one:
|
||||||
}}>
|
</Typography.Text>
|
||||||
<Chevron name={collapsed ? 'chevron-up' : 'chevron-down'} size={12} />
|
<ElementsInspector
|
||||||
Multiple elements found at the target coordinates
|
onElementSelected={(key: string) => {
|
||||||
</MultipleSelectorSectionTitle>
|
setSelectedId(key);
|
||||||
{!collapsed && (
|
onElementSelected(key);
|
||||||
<ElementsInspector
|
}}
|
||||||
onElementSelected={(key: string) => {
|
onElementHovered={onElementHovered}
|
||||||
setSelectedId(key);
|
onElementExpanded={() => {}}
|
||||||
onElementSelected(key);
|
root={null}
|
||||||
}}
|
selected={selectedId}
|
||||||
onElementHovered={onElementHovered}
|
elements={elements}
|
||||||
onElementExpanded={() => {}}
|
scrollable={false}
|
||||||
root={null}
|
/>
|
||||||
selected={selectedId}
|
|
||||||
elements={elements}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</MultipleSelectorSectionContainer>
|
</MultipleSelectorSectionContainer>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,30 +40,6 @@ test('rendering a single element', () => {
|
|||||||
expect(res.queryAllByText(name).length).toBe(1);
|
expect(res.queryAllByText(name).length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('collapsing an element', () => {
|
|
||||||
const id = 'id1';
|
|
||||||
const name = 'id_name';
|
|
||||||
const element: Element = {...dummyElmentData, id, name};
|
|
||||||
const res = render(
|
|
||||||
<MultipleSelectorSection
|
|
||||||
initialSelectedElement={null}
|
|
||||||
elements={{[id]: element}}
|
|
||||||
onElementSelected={() => {}}
|
|
||||||
onElementHovered={null}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(res.queryAllByText(name).length).toBe(1);
|
|
||||||
|
|
||||||
// collapse the view
|
|
||||||
fireEvent.click(res.getByText(TITLE_STRING));
|
|
||||||
expect(res.queryAllByText(name).length).toBe(0);
|
|
||||||
|
|
||||||
// re-expand the view
|
|
||||||
fireEvent.click(res.getByText(TITLE_STRING));
|
|
||||||
expect(res.queryAllByText(name).length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clicking on elements', () => {
|
test('clicking on elements', () => {
|
||||||
const ids = ['id1', 'id2', 'id3'];
|
const ids = ['id1', 'id2', 'id3'];
|
||||||
const names = ['id_name_first', 'id_name_second', 'id_name_third'];
|
const names = ['id_name_first', 'id_name_second', 'id_name_third'];
|
||||||
|
|||||||
Reference in New Issue
Block a user