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:
committed by
Facebook Github Bot
parent
b70a18cef2
commit
c21875e168
@@ -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';
|
||||
|
||||
212
src/plugins/layout/layout2/Inspector.js
Normal file
212
src/plugins/layout/layout2/Inspector.js
Normal 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;
|
||||
}
|
||||
}
|
||||
132
src/plugins/layout/layout2/index.js
Normal file
132
src/plugins/layout/layout2/index.js
Normal 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" />
|
||||
<strong>Version 2.0:</strong> Provide feedback about this plugin
|
||||
in our
|
||||
<Link href="https://fb.workplace.com/groups/246035322947653/">
|
||||
feedback group
|
||||
</Link>.
|
||||
</Toolbar>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user