refactoring

Summary:
This is refactoring the layout inspector. The old layout inspector was a single file with more than 1200 LOC which was really hard to debug and extend. This aims for splitting it up into smaller, easier to maintain components.

This version of the layout inspector only shows the view hierarchy for the regular view tree and the a11y tree. Additional features are added in stacked diffs.

Reviewed By: jknoxville

Differential Revision: D14100536

fbshipit-source-id: ca5e22dbb6ed9e34ce208a2a699ebfeb083904ad
This commit is contained in:
Daniel Büchele
2019-02-18 04:53:54 -08:00
committed by Facebook Github Bot
parent b70a18cef2
commit c21875e168
3 changed files with 345 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ export {
FlipperPlugin,
FlipperDevicePlugin,
} from './plugin.js';
export type {PluginClient} from './plugin.js';
export {clipboard} from 'electron';
export * from './fb-stubs/constants.js';
export * from './utils/createPaste.js';

View File

@@ -0,0 +1,212 @@
/**
* 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 {ElementID, Element, PluginClient} from 'flipper';
import {ElementsInspector} from 'flipper';
import {Component} from 'react';
import debounce from 'lodash.debounce';
import type {PersistedState} from './';
type GetNodesOptions = {
force?: boolean,
ax?: boolean,
forAccessibilityEvent?: boolean,
};
type Props = {
ax?: boolean,
client: PluginClient,
showsSidebar: boolean,
selectedElement: ?ElementID,
selectedAXElement: ?ElementID,
onSelect: (ids: ?ElementID) => void,
onDataValueChanged: (path: Array<string>, value: any) => void,
setPersistedState: (state: $Shape<PersistedState>) => void,
persistedState: PersistedState,
};
export default class Inspector extends Component<Props> {
call() {
return {
GET_ROOT: this.props.ax ? 'getAXRoot' : 'getRoot',
INVALIDATE: this.props.ax ? 'invalidateAX' : 'invalidate',
GET_NODES: this.props.ax ? 'getAXNodes' : 'getNodes',
SET_HIGHLIGHTED: 'setHighlighted',
SELECT: this.props.ax ? 'selectAX' : 'select',
};
}
selected = () => {
return this.props.ax
? this.props.selectedAXElement
: this.props.selectedElement;
};
root = () => {
return this.props.ax
? this.props.persistedState.rootAXElement
: this.props.persistedState.rootElement;
};
elements = () => {
return this.props.ax
? this.props.persistedState.AXelements
: this.props.persistedState.elements;
};
componentDidMount() {
this.props.client.call(this.call().GET_ROOT).then((root: Element) => {
this.props.setPersistedState({
[this.props.ax ? 'rootAXElement' : 'rootElement']: root.id,
});
this.updateElement(root.id, {...root, expanded: true});
this.performInitialExpand(root);
});
this.props.client.subscribe(
this.call().INVALIDATE,
({nodes}: {nodes: Array<{id: ElementID}>}) => {
this.getNodes(nodes.map(n => n.id), {});
},
);
}
componentDidUpdate(prevProps: Props) {
const {ax, selectedElement, selectedAXElement} = this.props;
if (
ax &&
selectedElement !== prevProps.selectedElement &&
selectedElement
) {
// selected element changed, find linked AX element
const linkedAXNode: ?ElementID = this.props.persistedState.elements[
selectedElement
]?.extraInfo?.linkedAXNode;
this.props.onSelect(linkedAXNode);
} else if (
!ax &&
selectedAXElement !== prevProps.selectedAXElement &&
selectedAXElement
) {
// selected AX element changed, find linked element
// $FlowFixMe Object.values retunes mixed type
const linkedNode: ?Element = Object.values(
this.props.persistedState.elements,
// $FlowFixMe it's an Element not mixed
).find((e: Element) => e.extraInfo?.linkedAXNode === selectedAXElement);
this.props.onSelect(linkedNode?.id);
}
}
updateElement(id: ElementID, data: Object) {
this.props.setPersistedState({
[this.props.ax ? 'AXelements' : 'elements']: {
...this.elements(),
[id]: {
...this.elements()[id],
...data,
},
},
});
}
// When opening the inspector for the first time, expand all elements that
// contain only 1 child recursively.
async performInitialExpand(element: Element): Promise<void> {
if (!element.children.length) {
// element has no children so we're as deep as we can be
return;
}
return this.getChildren(element.id, {}, true).then(
(elements: Array<Element>) => {
if (element.children.length >= 2) {
// element has two or more children so we can stop expanding
return;
}
return this.performInitialExpand(this.elements()[element.children[0]]);
},
);
}
async getChildren(
id: ElementID,
options: GetNodesOptions,
expanded?: boolean,
): Promise<Array<Element>> {
if (!this.elements()[id]) {
await this.getNodes([id], options, expanded);
}
return this.getNodes(this.elements()[id].children, options, expanded);
}
getNodes(
ids: Array<ElementID> = [],
options: GetNodesOptions,
expanded?: boolean,
): Promise<Array<Element>> {
const {forAccessibilityEvent} = options;
if (ids.length > 0) {
return this.props.client
.call(this.call().GET_NODES, {
ids,
forAccessibilityEvent,
selected: false,
})
.then(({elements}) => {
elements.forEach(e => {
if (typeof expanded === 'boolean') {
e.expanded = expanded;
}
this.updateElement(e.id, e);
});
return elements;
});
} else {
return Promise.resolve([]);
}
}
onElementSelected = debounce((selectedKey: ElementID) => {
this.onElementHovered(selectedKey);
this.props.onSelect(selectedKey);
});
onElementHovered = debounce((key: ?ElementID) =>
this.props.client.call(this.call().SET_HIGHLIGHTED, {
id: key,
}),
);
onElementExpanded = (id: ElementID, deep: boolean) => {
const expanded = !this.elements()[id].expanded;
this.updateElement(id, {expanded});
if (expanded) {
this.getChildren(id, {}).then(children => {
if (deep) {
children.forEach(child => this.onElementExpanded(child.id, deep));
}
});
}
};
render() {
return this.root() ? (
<ElementsInspector
onElementSelected={this.onElementSelected}
onElementHovered={this.onElementHovered}
onElementExpanded={this.onElementExpanded}
onValueChanged={this.props.onDataValueChanged}
selected={this.selected()}
root={this.root()}
elements={this.elements()}
/>
) : null;
}
}

View File

@@ -0,0 +1,132 @@
/**
* 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 {ElementID, Element} from 'flipper';
import {
FlexColumn,
FlexRow,
FlipperPlugin,
Toolbar,
Sidebar,
Link,
Glyph,
} from 'flipper';
import Inspector from './Inspector';
import ToolbarIcon from './ToolbarIcon';
type State = {|
init: boolean,
inAXMode: boolean,
selectedElement: ?ElementID,
selectedAXElement: ?ElementID,
|};
export type PersistedState = {|
rootElement: ?ElementID,
rootAXElement: ?ElementID,
elements: {[key: ElementID]: Element},
AXelements: {[key: ElementID]: Element},
|};
export default class Layout extends FlipperPlugin<State, void, PersistedState> {
static defaultPersistedState = {
rootElement: null,
rootAXElement: null,
elements: {},
AXelements: {},
};
state = {
init: false,
inAXMode: false,
selectedElement: null,
selectedAXElement: null,
};
componentDidMount() {
this.props.setPersistedState(Layout.defaultPersistedState);
}
init() {
this.setState({init: true});
}
onToggleAXMode = () => {
this.setState({inAXMode: !this.state.inAXMode});
};
onDataValueChanged = (path: Array<string>, value: any) => {
const id = this.state.inAXMode
? this.state.selectedAXElement
: this.state.selectedElement;
this.client.call('setData', {
id,
path,
value,
ax: this.state.inAXMode,
});
};
render() {
const inspectorProps = {
client: this.client,
selectedElement: this.state.selectedElement,
selectedAXElement: this.state.selectedAXElement,
setPersistedState: this.props.setPersistedState,
persistedState: this.props.persistedState,
onDataValueChanged: this.onDataValueChanged,
};
return (
<FlexColumn grow={true}>
{this.state.init && (
<>
<Toolbar>
{this.realClient.query.os === 'Android' && (
<ToolbarIcon
onClick={this.onToggleAXMode}
title="Toggle to see the accessibility hierarchy"
icon="accessibility"
active={this.state.inAXMode}
/>
)}
</Toolbar>
<FlexRow grow={true}>
<Inspector
{...inspectorProps}
onSelect={selectedElement => this.setState({selectedElement})}
showsSidebar={!this.state.inAXMode}
/>
{this.state.inAXMode && (
<Sidebar position="right" width={400}>
<Inspector
{...inspectorProps}
onSelect={selectedAXElement =>
this.setState({selectedAXElement})
}
showsSidebar={true}
ax
/>
</Sidebar>
)}
</FlexRow>
</>
)}
<Toolbar position="bottom" compact>
<Glyph name="beta" color="#8157C7" />&nbsp;
<strong>Version 2.0:</strong>&nbsp; Provide feedback about this plugin
in our&nbsp;
<Link href="https://fb.workplace.com/groups/246035322947653/">
feedback group
</Link>.
</Toolbar>
</FlexColumn>
);
}
}