Two-tree view up and running, separately interactive/editable

Summary: Added duplicate view tree (will be replaced with accessibility node tree eventually). Can toggle ax mode on and off and interact with each tree individually to view/change properties.

Reviewed By: danielbuechele

Differential Revision: D8717557

fbshipit-source-id: 1109ccafd49b6958ee7a70c2e8851ed8351516ae
This commit is contained in:
Sara Valderrama
2018-07-05 16:18:32 -07:00
committed by Facebook Github Bot
parent 917376db6d
commit d8cf48d750
3 changed files with 209 additions and 81 deletions

View File

@@ -22,20 +22,23 @@ import {
SearchInput, SearchInput,
SearchIcon, SearchIcon,
SonarSidebar, SonarSidebar,
VerticalRule,
} from 'sonar'; } from 'sonar';
import {AXElementsInspector} from '../../fb-stubs/AXLayoutExtender.js'; import {AXElementsInspector} from '../../fb-stubs/AXLayoutExtender.js';
import config from '../../fb-stubs/config.js';
// $FlowFixMe // $FlowFixMe
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
export type InspectorState = {| export type InspectorState = {|
initialised: boolean, initialised: boolean,
AXinitialised: boolean,
selected: ?ElementID, selected: ?ElementID,
selectedAX: ?ElementID, AXselected: ?ElementID,
root: ?ElementID, root: ?ElementID,
AXroot: ?ElementID,
elements: {[key: ElementID]: Element}, elements: {[key: ElementID]: Element},
AXelements: {[key: ElementID]: Element},
isSearchActive: boolean, isSearchActive: boolean,
inAXMode: boolean, inAXMode: boolean,
searchResults: ?ElementSearchResultSet, searchResults: ?ElementSearchResultSet,
@@ -143,12 +146,15 @@ export default class Layout extends SonarPlugin<InspectorState> {
state = { state = {
elements: {}, elements: {},
AXelements: {},
initialised: false, initialised: false,
AXinitialised: false,
isSearchActive: false, isSearchActive: false,
inAXMode: false, inAXMode: false,
root: null, root: null,
AXroot: null,
selected: null, selected: null,
selectedAX: null, AXselected: null,
searchResults: null, searchResults: null,
outstandingSearchQuery: null, outstandingSearchQuery: null,
}; };
@@ -162,7 +168,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
SelectAXElement(state: InspectorState, {key}: SelectElementArgs) { SelectAXElement(state: InspectorState, {key}: SelectElementArgs) {
return { return {
selectedAX: key, AXselected: key,
}; };
}, },
@@ -178,6 +184,18 @@ export default class Layout extends SonarPlugin<InspectorState> {
}; };
}, },
ExpandAXElement(state: InspectorState, {expand, key}: ExpandElementArgs) {
return {
AXelements: {
...state.AXelements,
[key]: {
...state.AXelements[key],
expanded: expand,
},
},
};
},
ExpandElements(state: InspectorState, {elements}: ExpandElementsArgs) { ExpandElements(state: InspectorState, {elements}: ExpandElementsArgs) {
const expandedSet = new Set(elements); const expandedSet = new Set(elements);
const newState = { const newState = {
@@ -194,6 +212,22 @@ export default class Layout extends SonarPlugin<InspectorState> {
return newState; return newState;
}, },
ExpandAXElements(state: InspectorState, {elements}: ExpandElementsArgs) {
const expandedSet = new Set(elements);
const newState = {
AXelements: {
...state.AXelements,
},
};
for (const key of Object.keys(state.AXelements)) {
newState.AXelements[key] = {
...newState.AXelements[key],
expanded: expandedSet.has(key),
};
}
return newState;
},
UpdateElements(state: InspectorState, {elements}: UpdateElementsArgs) { UpdateElements(state: InspectorState, {elements}: UpdateElementsArgs) {
const updatedElements = state.elements; const updatedElements = state.elements;
@@ -209,10 +243,29 @@ export default class Layout extends SonarPlugin<InspectorState> {
return {elements: updatedElements}; return {elements: updatedElements};
}, },
UpdateAXElements(state: InspectorState, {elements}: UpdateElementsArgs) {
const updatedElements = state.AXelements;
for (const element of elements) {
const current = updatedElements[element.id] || {};
// $FlowFixMe
updatedElements[element.id] = {
...current,
...element,
};
}
return {AXelements: updatedElements};
},
SetRoot(state: InspectorState, {root}: SetRootArgs) { SetRoot(state: InspectorState, {root}: SetRootArgs) {
return {root}; return {root};
}, },
SetAXRoot(state: InspectorState, {root}: SetRootArgs) {
return {AXroot: root};
},
SetSearchActive( SetSearchActive(
state: InspectorState, state: InspectorState,
{isSearchActive}: {isSearchActive: boolean}, {isSearchActive}: {isSearchActive: boolean},
@@ -240,7 +293,9 @@ export default class Layout extends SonarPlugin<InspectorState> {
executeCommand(command: string) { executeCommand(command: string) {
return this.client.call('executeCommand', { return this.client.call('executeCommand', {
command: command, command: command,
context: this.state.selected, context: this.state.inAXMode
? this.state.AXselected
: this.state.selected,
}); });
} }
@@ -248,16 +303,23 @@ export default class Layout extends SonarPlugin<InspectorState> {
* When opening the inspector for the first time, expand all elements that contain only 1 child * When opening the inspector for the first time, expand all elements that contain only 1 child
* recursively. * recursively.
*/ */
async performInitialExpand(element: Element): Promise<void> { async performInitialExpand(element: Element, ax: boolean): Promise<void> {
if (!element.children.length) { if (!element.children.length) {
// element has no children so we're as deep as we can be // element has no children so we're as deep as we can be
return; return;
} }
this.dispatchAction({expand: true, key: element.id, type: 'ExpandElement'}); this.dispatchAction({
expand: true,
key: element.id,
type: ax ? 'ExpandAXElement' : 'ExpandElement',
});
return this.getChildren(element.id).then((elements: Array<Element>) => { return this.getChildren(element.id, ax).then((elements: Array<Element>) => {
this.dispatchAction({elements, type: 'UpdateElements'}); this.dispatchAction({
elements,
type: ax ? 'UpdateAXElements' : 'UpdateElements',
});
if (element.children.length >= 2) { if (element.children.length >= 2) {
// element has two or more children so we can stop expanding // element has two or more children so we can stop expanding
@@ -265,7 +327,10 @@ export default class Layout extends SonarPlugin<InspectorState> {
} }
return this.performInitialExpand( return this.performInitialExpand(
this.state.elements[element.children[0]], ax
? this.state.AXelements[element.children[0]]
: this.state.elements[element.children[0]],
ax,
); );
}); });
} }
@@ -330,25 +395,36 @@ export default class Layout extends SonarPlugin<InspectorState> {
this.client.call('getRoot').then((element: Element) => { this.client.call('getRoot').then((element: Element) => {
this.dispatchAction({elements: [element], type: 'UpdateElements'}); this.dispatchAction({elements: [element], type: 'UpdateElements'});
this.dispatchAction({root: element.id, type: 'SetRoot'}); this.dispatchAction({root: element.id, type: 'SetRoot'});
this.performInitialExpand(element).then(() => { this.performInitialExpand(element, false).then(() => {
this.props.logger.trackTimeSince('LayoutInspectorInitialize'); this.props.logger.trackTimeSince('LayoutInspectorInitialize');
this.setState({initialised: true}); this.setState({initialised: true});
}); });
}); });
this.client.call('getRoot').then((element: Element) => {
this.dispatchAction({elements: [element], type: 'UpdateAXElements'});
this.dispatchAction({root: element.id, type: 'SetAXRoot'});
this.performInitialExpand(element, true).then(() => {
this.setState({AXinitialised: true});
});
});
this.client.subscribe( this.client.subscribe(
'invalidate', 'invalidate',
({nodes}: {nodes: Array<{id: ElementID}>}) => { ({nodes}: {nodes: Array<{id: ElementID}>}) => {
this.invalidate(nodes.map(node => node.id)).then( this.invalidate(nodes.map(node => node.id)).then(
(elements: Array<Element>) => { (elements: Array<Element>) => {
this.dispatchAction({elements, type: 'UpdateElements'}); this.dispatchAction({elements, type: 'UpdateElements'});
// to be removed once trees are separate - will have own invalidate
this.dispatchAction({elements, type: 'UpdateAXElements'});
}, },
); );
}, },
); );
this.client.subscribe('select', ({path}: {path: Array<ElementID>}) => { this.client.subscribe('select', ({path}: {path: Array<ElementID>}) => {
this.getNodesAndDirectChildren(path).then((elements: Array<Element>) => { this.getNodesAndDirectChildren(path, false).then(
(elements: Array<Element>) => {
const selected = path[path.length - 1]; const selected = path[path.length - 1];
this.dispatchAction({elements, type: 'UpdateElements'}); this.dispatchAction({elements, type: 'UpdateElements'});
@@ -361,7 +437,8 @@ export default class Layout extends SonarPlugin<InspectorState> {
this.client.send('setHighlighted', {id: selected}); this.client.send('setHighlighted', {id: selected});
this.client.send('setSearchActive', {active: false}); this.client.send('setSearchActive', {active: false});
}); },
);
}); });
} }
@@ -370,7 +447,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
return Promise.resolve([]); return Promise.resolve([]);
} }
return this.getNodes(ids, true).then((elements: Array<Element>) => { return this.getNodes(ids, true, false).then((elements: Array<Element>) => {
const children = elements const children = elements
.filter(element => { .filter(element => {
const prev = this.state.elements[element.id]; const prev = this.state.elements[element.id];
@@ -385,13 +462,16 @@ export default class Layout extends SonarPlugin<InspectorState> {
}); });
} }
getNodesAndDirectChildren(ids: Array<ElementID>): Promise<Array<Element>> { getNodesAndDirectChildren(
return this.getNodes(ids, false).then((elements: Array<Element>) => { ids: Array<ElementID>,
ax: boolean,
): Promise<Array<Element>> {
return this.getNodes(ids, false, ax).then((elements: Array<Element>) => {
const children = elements const children = elements
.map(element => element.children) .map(element => element.children)
.reduce((acc, val) => acc.concat(val), []); .reduce((acc, val) => acc.concat(val), []);
return Promise.all([elements, this.getNodes(children, false)]).then( return Promise.all([elements, this.getNodes(children, false, ax)]).then(
arr => { arr => {
return arr.reduce((acc, val) => acc.concat(val), []); return arr.reduce((acc, val) => acc.concat(val), []);
}, },
@@ -399,17 +479,24 @@ export default class Layout extends SonarPlugin<InspectorState> {
}); });
} }
getChildren(key: ElementID): Promise<Array<Element>> { getChildren(key: ElementID, ax: boolean): Promise<Array<Element>> {
return this.getNodes(this.state.elements[key].children, false); return this.getNodes(
(ax ? this.state.AXelements : this.state.elements)[key].children,
false,
ax,
);
} }
getNodes( getNodes(
ids: Array<ElementID> = [], ids: Array<ElementID> = [],
force: boolean, force: boolean,
ax: boolean,
): Promise<Array<Element>> { ): Promise<Array<Element>> {
if (!force) { if (!force) {
ids = ids.filter(id => { ids = ids.filter(id => {
return this.state.elements[id] === undefined; return (
(ax ? this.state.AXelements : this.state.elements)[id] === undefined
);
}); });
} }
@@ -426,25 +513,35 @@ export default class Layout extends SonarPlugin<InspectorState> {
} }
} }
isExpanded(key: ElementID): boolean { isExpanded(key: ElementID, ax: boolean): boolean {
return this.state.elements[key].expanded; return ax
? this.state.AXelements[key].expanded
: this.state.elements[key].expanded;
} }
expandElement = (key: ElementID): Promise<Array<Element>> => { expandElement = (key: ElementID, ax: boolean): Promise<Array<Element>> => {
const expand = !this.isExpanded(key); const expand = !this.isExpanded(key, ax);
return this.setElementExpanded(key, expand); return this.setElementExpanded(key, expand, ax);
}; };
setElementExpanded = ( setElementExpanded = (
key: ElementID, key: ElementID,
expand: boolean, expand: boolean,
ax: boolean,
): Promise<Array<Element>> => { ): Promise<Array<Element>> => {
this.dispatchAction({expand, key, type: 'ExpandElement'}); this.dispatchAction({
expand,
key,
type: ax ? 'ExpandAXElement' : 'ExpandElement',
});
performance.mark('LayoutInspectorExpandElement'); performance.mark('LayoutInspectorExpandElement');
if (expand) { if (expand) {
return this.getChildren(key).then((elements: Array<Element>) => { return this.getChildren(key, ax).then((elements: Array<Element>) => {
this.props.logger.trackTimeSince('LayoutInspectorExpandElement'); this.props.logger.trackTimeSince('LayoutInspectorExpandElement');
this.dispatchAction({elements, type: 'UpdateElements'}); this.dispatchAction({
elements,
type: ax ? 'UpdateAXElements' : 'UpdateElements',
});
return Promise.resolve(elements); return Promise.resolve(elements);
}); });
} else { } else {
@@ -452,11 +549,11 @@ export default class Layout extends SonarPlugin<InspectorState> {
} }
}; };
deepExpandElement = async (key: ElementID) => { deepExpandElement = async (key: ElementID, ax: boolean) => {
const expand = !this.isExpanded(key); const expand = !this.isExpanded(key, ax);
if (!expand) { if (!expand) {
// we never deep unexpand // we never deep unexpand
return this.setElementExpanded(key, false); return this.setElementExpanded(key, false, ax);
} }
// queue of keys to open // queue of keys to open
@@ -469,7 +566,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
const key = keys.shift(); const key = keys.shift();
// expand current element // expand current element
const children = await this.setElementExpanded(key, true); const children = await this.setElementExpanded(key, true, ax);
// and add it's children to the queue // and add it's children to the queue
for (const child of children) { for (const child of children) {
@@ -482,9 +579,21 @@ export default class Layout extends SonarPlugin<InspectorState> {
onElementExpanded = (key: ElementID, deep: boolean) => { onElementExpanded = (key: ElementID, deep: boolean) => {
if (deep) { if (deep) {
this.deepExpandElement(key); this.deepExpandElement(key, false);
} else { } else {
this.expandElement(key); this.expandElement(key, false);
}
this.props.logger.track('usage', 'layout:element-expanded', {
id: key,
deep: deep,
});
};
onAXElementExpanded = (key: ElementID, deep: boolean) => {
if (deep) {
this.deepExpandElement(key, true);
} else {
this.expandElement(key, true);
} }
this.props.logger.track('usage', 'layout:element-expanded', { this.props.logger.track('usage', 'layout:element-expanded', {
id: key, id: key,
@@ -498,20 +607,15 @@ export default class Layout extends SonarPlugin<InspectorState> {
this.client.send('setSearchActive', {active: isSearchActive}); this.client.send('setSearchActive', {active: isSearchActive});
}; };
onTestToggleAccessibility = () => { onToggleAccessibility = () => {
const inAXMode = !this.state.inAXMode; const inAXMode = !this.state.inAXMode;
this.dispatchAction({inAXMode, type: 'SetAXMode'}); this.dispatchAction({inAXMode, type: 'SetAXMode'});
this.client
.call('testAccessibility', {active: inAXMode})
.then(({message}: {message: string}) => {
console.log(message);
});
}; };
onElementSelected = debounce((key: ElementID) => { onElementSelected = debounce((key: ElementID) => {
this.dispatchAction({key, type: 'SelectElement'}); this.dispatchAction({key, type: 'SelectElement'});
this.client.send('setHighlighted', {id: key}); this.client.send('setHighlighted', {id: key});
this.getNodes([key], true).then((elements: Array<Element>) => { this.getNodes([key], true, false).then((elements: Array<Element>) => {
this.dispatchAction({elements, type: 'UpdateElements'}); this.dispatchAction({elements, type: 'UpdateElements'});
}); });
}); });
@@ -519,8 +623,8 @@ export default class Layout extends SonarPlugin<InspectorState> {
onAXElementSelected = debounce((key: ElementID) => { onAXElementSelected = debounce((key: ElementID) => {
this.dispatchAction({key, type: 'SelectAXElement'}); this.dispatchAction({key, type: 'SelectAXElement'});
this.client.send('setHighlighted', {id: key}); this.client.send('setHighlighted', {id: key});
this.getNodes([key], true).then((elements: Array<Element>) => { this.getNodes([key], true, true).then((elements: Array<Element>) => {
this.dispatchAction({elements, type: 'UpdateElements'}); this.dispatchAction({elements, type: 'UpdateAXElements'});
}); });
}); });
@@ -530,7 +634,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
onDataValueChanged = (path: Array<string>, value: any) => { onDataValueChanged = (path: Array<string>, value: any) => {
const selected = this.state.inAXMode const selected = this.state.inAXMode
? this.state.selectedAX ? this.state.AXselected
: this.state.selected; : this.state.selected;
this.client.send('setData', {id: selected, path, value}); this.client.send('setData', {id: selected, path, value});
this.props.logger.track('usage', 'layout:value-changed', { this.props.logger.track('usage', 'layout:value-changed', {
@@ -541,32 +645,41 @@ export default class Layout extends SonarPlugin<InspectorState> {
}; };
renderSidebar = () => { renderSidebar = () => {
return this.state.selected != null ? ( if (this.state.inAXMode) {
// empty if no element selected w/in AX node tree
return (
this.state.AXselected && (
<InspectorSidebar
element={this.state.AXelements[this.state.AXselected]}
onValueChanged={this.onDataValueChanged}
client={this.client}
/>
)
);
} else {
// empty if no element selected w/in view tree
return (
this.state.selected != null && (
<InspectorSidebar <InspectorSidebar
element={this.state.elements[this.state.selected]} element={this.state.elements[this.state.selected]}
onValueChanged={this.onDataValueChanged} onValueChanged={this.onDataValueChanged}
client={this.client} client={this.client}
/> />
) : null; )
}; );
}
renderAXSidebar = () => {
return this.state.selectedAX != null ? (
<InspectorSidebar
element={this.state.elements[this.state.selectedAX]}
onValueChanged={this.onDataValueChanged}
client={this.client}
/>
) : null;
}; };
render() { render() {
const { const {
initialised, initialised,
AXinitialised,
selected, selected,
selectedAX, AXselected,
root, root,
AXroot,
elements, elements,
AXelements,
isSearchActive, isSearchActive,
inAXMode, inAXMode,
outstandingSearchQuery, outstandingSearchQuery,
@@ -576,12 +689,12 @@ export default class Layout extends SonarPlugin<InspectorState> {
<AXElementsInspector <AXElementsInspector
onElementSelected={this.onAXElementSelected} onElementSelected={this.onAXElementSelected}
onElementHovered={this.onElementHovered} onElementHovered={this.onElementHovered}
onElementExpanded={this.onElementExpanded} onElementExpanded={this.onAXElementExpanded}
onValueChanged={this.onDataValueChanged} onValueChanged={this.onDataValueChanged}
selected={selectedAX} selected={AXselected}
searchResults={this.state.searchResults} searchResults={this.state.searchResults}
root={root} root={AXroot}
elements={elements} elements={AXelements}
/> />
); );
const AXButtonVisible = AXInspector !== null; const AXButtonVisible = AXInspector !== null;
@@ -590,7 +703,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
<FlexColumn fill={true}> <FlexColumn fill={true}>
<Toolbar> <Toolbar>
<SearchIconContainer <SearchIconContainer
onClick={this.onFindClick} onClick={inAXMode ? null : this.onFindClick}
role="button" role="button"
tabIndex={-1} tabIndex={-1}
title="Select an element on the device to inspect it"> title="Select an element on the device to inspect it">
@@ -606,7 +719,7 @@ export default class Layout extends SonarPlugin<InspectorState> {
</SearchIconContainer> </SearchIconContainer>
{AXButtonVisible ? ( {AXButtonVisible ? (
<SearchIconContainer <SearchIconContainer
onClick={this.onTestToggleAccessibility} onClick={this.onToggleAccessibility}
role="button" role="button"
tabIndex={-1} tabIndex={-1}
title="Toggle accessibility mode within the LayoutInspector"> title="Toggle accessibility mode within the LayoutInspector">
@@ -648,11 +761,10 @@ export default class Layout extends SonarPlugin<InspectorState> {
<LoadingIndicator /> <LoadingIndicator />
</Center> </Center>
)} )}
{initialised && inAXMode ? AXInspector : null} {AXinitialised && inAXMode ? <VerticalRule /> : null}
{AXinitialised && inAXMode ? AXInspector : null}
</FlexRow> </FlexRow>
<SonarSidebar> <SonarSidebar>{this.renderSidebar()}</SonarSidebar>
{inAXMode ? this.renderAXSidebar() : this.renderSidebar()}
</SonarSidebar>
</FlexColumn> </FlexColumn>
); );
} }

View File

@@ -0,0 +1,14 @@
/**
* 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 styled from '../styled/index.js';
export default styled.view({
backgroundColor: '#c9ced4',
width: 3,
margin: '0',
});

View File

@@ -128,6 +128,7 @@ export {default as ResizeSensor} from './components/ResizeSensor.js';
// typhography // typhography
export {default as HorizontalRule} from './components/HorizontalRule.js'; export {default as HorizontalRule} from './components/HorizontalRule.js';
export {default as VerticalRule} from './components/VerticalRule.js';
export {default as Label} from './components/Label.js'; export {default as Label} from './components/Label.js';
export {default as Heading} from './components/Heading.js'; export {default as Heading} from './components/Heading.js';
@@ -154,6 +155,7 @@ export type {
Element, Element,
ElementSearchResultSet, ElementSearchResultSet,
} from './components/elements-inspector/ElementsInspector.js'; } from './components/elements-inspector/ElementsInspector.js';
export {Elements} from './components/elements-inspector/elements.js';
export { export {
default as ElementsInspector, default as ElementsInspector,
} from './components/elements-inspector/ElementsInspector.js'; } from './components/elements-inspector/ElementsInspector.js';