From d8cf48d7500efd2781c5e23c803a27e10bcc7e7b Mon Sep 17 00:00:00 2001 From: Sara Valderrama Date: Thu, 5 Jul 2018 16:18:32 -0700 Subject: [PATCH] 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 --- src/plugins/layout/index.js | 274 +++++++++++++++++++++--------- src/ui/components/VerticalRule.js | 14 ++ src/ui/index.js | 2 + 3 files changed, 209 insertions(+), 81 deletions(-) create mode 100644 src/ui/components/VerticalRule.js diff --git a/src/plugins/layout/index.js b/src/plugins/layout/index.js index 67c2afef7..a70bd3957 100644 --- a/src/plugins/layout/index.js +++ b/src/plugins/layout/index.js @@ -22,20 +22,23 @@ import { SearchInput, SearchIcon, SonarSidebar, + VerticalRule, } from 'sonar'; import {AXElementsInspector} from '../../fb-stubs/AXLayoutExtender.js'; -import config from '../../fb-stubs/config.js'; // $FlowFixMe import debounce from 'lodash.debounce'; export type InspectorState = {| initialised: boolean, + AXinitialised: boolean, selected: ?ElementID, - selectedAX: ?ElementID, + AXselected: ?ElementID, root: ?ElementID, + AXroot: ?ElementID, elements: {[key: ElementID]: Element}, + AXelements: {[key: ElementID]: Element}, isSearchActive: boolean, inAXMode: boolean, searchResults: ?ElementSearchResultSet, @@ -143,12 +146,15 @@ export default class Layout extends SonarPlugin { state = { elements: {}, + AXelements: {}, initialised: false, + AXinitialised: false, isSearchActive: false, inAXMode: false, root: null, + AXroot: null, selected: null, - selectedAX: null, + AXselected: null, searchResults: null, outstandingSearchQuery: null, }; @@ -162,7 +168,7 @@ export default class Layout extends SonarPlugin { SelectAXElement(state: InspectorState, {key}: SelectElementArgs) { return { - selectedAX: key, + AXselected: key, }; }, @@ -178,6 +184,18 @@ export default class Layout extends SonarPlugin { }; }, + ExpandAXElement(state: InspectorState, {expand, key}: ExpandElementArgs) { + return { + AXelements: { + ...state.AXelements, + [key]: { + ...state.AXelements[key], + expanded: expand, + }, + }, + }; + }, + ExpandElements(state: InspectorState, {elements}: ExpandElementsArgs) { const expandedSet = new Set(elements); const newState = { @@ -194,6 +212,22 @@ export default class Layout extends SonarPlugin { 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) { const updatedElements = state.elements; @@ -209,10 +243,29 @@ export default class Layout extends SonarPlugin { 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) { return {root}; }, + SetAXRoot(state: InspectorState, {root}: SetRootArgs) { + return {AXroot: root}; + }, + SetSearchActive( state: InspectorState, {isSearchActive}: {isSearchActive: boolean}, @@ -240,7 +293,9 @@ export default class Layout extends SonarPlugin { executeCommand(command: string) { return this.client.call('executeCommand', { 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 { * When opening the inspector for the first time, expand all elements that contain only 1 child * recursively. */ - async performInitialExpand(element: Element): Promise { + async performInitialExpand(element: Element, ax: boolean): Promise { if (!element.children.length) { // element has no children so we're as deep as we can be 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) => { - this.dispatchAction({elements, type: 'UpdateElements'}); + return this.getChildren(element.id, ax).then((elements: Array) => { + this.dispatchAction({ + elements, + type: ax ? 'UpdateAXElements' : 'UpdateElements', + }); if (element.children.length >= 2) { // element has two or more children so we can stop expanding @@ -265,7 +327,10 @@ export default class Layout extends SonarPlugin { } return this.performInitialExpand( - this.state.elements[element.children[0]], + ax + ? this.state.AXelements[element.children[0]] + : this.state.elements[element.children[0]], + ax, ); }); } @@ -330,38 +395,50 @@ export default class Layout extends SonarPlugin { this.client.call('getRoot').then((element: Element) => { this.dispatchAction({elements: [element], type: 'UpdateElements'}); this.dispatchAction({root: element.id, type: 'SetRoot'}); - this.performInitialExpand(element).then(() => { + this.performInitialExpand(element, false).then(() => { this.props.logger.trackTimeSince('LayoutInspectorInitialize'); 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( 'invalidate', ({nodes}: {nodes: Array<{id: ElementID}>}) => { this.invalidate(nodes.map(node => node.id)).then( (elements: Array) => { 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}) => { - this.getNodesAndDirectChildren(path).then((elements: Array) => { - const selected = path[path.length - 1]; + this.getNodesAndDirectChildren(path, false).then( + (elements: Array) => { + const selected = path[path.length - 1]; - this.dispatchAction({elements, type: 'UpdateElements'}); - this.dispatchAction({key: selected, type: 'SelectElement'}); - this.dispatchAction({isSearchActive: false, type: 'SetSearchActive'}); + this.dispatchAction({elements, type: 'UpdateElements'}); + this.dispatchAction({key: selected, type: 'SelectElement'}); + this.dispatchAction({isSearchActive: false, type: 'SetSearchActive'}); - for (const key of path) { - this.dispatchAction({expand: true, key, type: 'ExpandElement'}); - } + for (const key of path) { + this.dispatchAction({expand: true, key, type: 'ExpandElement'}); + } - this.client.send('setHighlighted', {id: selected}); - this.client.send('setSearchActive', {active: false}); - }); + this.client.send('setHighlighted', {id: selected}); + this.client.send('setSearchActive', {active: false}); + }, + ); }); } @@ -370,7 +447,7 @@ export default class Layout extends SonarPlugin { return Promise.resolve([]); } - return this.getNodes(ids, true).then((elements: Array) => { + return this.getNodes(ids, true, false).then((elements: Array) => { const children = elements .filter(element => { const prev = this.state.elements[element.id]; @@ -385,13 +462,16 @@ export default class Layout extends SonarPlugin { }); } - getNodesAndDirectChildren(ids: Array): Promise> { - return this.getNodes(ids, false).then((elements: Array) => { + getNodesAndDirectChildren( + ids: Array, + ax: boolean, + ): Promise> { + return this.getNodes(ids, false, ax).then((elements: Array) => { const children = elements .map(element => element.children) .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 => { return arr.reduce((acc, val) => acc.concat(val), []); }, @@ -399,17 +479,24 @@ export default class Layout extends SonarPlugin { }); } - getChildren(key: ElementID): Promise> { - return this.getNodes(this.state.elements[key].children, false); + getChildren(key: ElementID, ax: boolean): Promise> { + return this.getNodes( + (ax ? this.state.AXelements : this.state.elements)[key].children, + false, + ax, + ); } getNodes( ids: Array = [], force: boolean, + ax: boolean, ): Promise> { if (!force) { 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 { } } - isExpanded(key: ElementID): boolean { - return this.state.elements[key].expanded; + isExpanded(key: ElementID, ax: boolean): boolean { + return ax + ? this.state.AXelements[key].expanded + : this.state.elements[key].expanded; } - expandElement = (key: ElementID): Promise> => { - const expand = !this.isExpanded(key); - return this.setElementExpanded(key, expand); + expandElement = (key: ElementID, ax: boolean): Promise> => { + const expand = !this.isExpanded(key, ax); + return this.setElementExpanded(key, expand, ax); }; setElementExpanded = ( key: ElementID, expand: boolean, + ax: boolean, ): Promise> => { - this.dispatchAction({expand, key, type: 'ExpandElement'}); + this.dispatchAction({ + expand, + key, + type: ax ? 'ExpandAXElement' : 'ExpandElement', + }); performance.mark('LayoutInspectorExpandElement'); if (expand) { - return this.getChildren(key).then((elements: Array) => { + return this.getChildren(key, ax).then((elements: Array) => { this.props.logger.trackTimeSince('LayoutInspectorExpandElement'); - this.dispatchAction({elements, type: 'UpdateElements'}); + this.dispatchAction({ + elements, + type: ax ? 'UpdateAXElements' : 'UpdateElements', + }); return Promise.resolve(elements); }); } else { @@ -452,11 +549,11 @@ export default class Layout extends SonarPlugin { } }; - deepExpandElement = async (key: ElementID) => { - const expand = !this.isExpanded(key); + deepExpandElement = async (key: ElementID, ax: boolean) => { + const expand = !this.isExpanded(key, ax); if (!expand) { // we never deep unexpand - return this.setElementExpanded(key, false); + return this.setElementExpanded(key, false, ax); } // queue of keys to open @@ -469,7 +566,7 @@ export default class Layout extends SonarPlugin { const key = keys.shift(); // 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 for (const child of children) { @@ -482,9 +579,21 @@ export default class Layout extends SonarPlugin { onElementExpanded = (key: ElementID, deep: boolean) => { if (deep) { - this.deepExpandElement(key); + this.deepExpandElement(key, false); } 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', { id: key, @@ -498,20 +607,15 @@ export default class Layout extends SonarPlugin { this.client.send('setSearchActive', {active: isSearchActive}); }; - onTestToggleAccessibility = () => { + onToggleAccessibility = () => { const inAXMode = !this.state.inAXMode; this.dispatchAction({inAXMode, type: 'SetAXMode'}); - this.client - .call('testAccessibility', {active: inAXMode}) - .then(({message}: {message: string}) => { - console.log(message); - }); }; onElementSelected = debounce((key: ElementID) => { this.dispatchAction({key, type: 'SelectElement'}); this.client.send('setHighlighted', {id: key}); - this.getNodes([key], true).then((elements: Array) => { + this.getNodes([key], true, false).then((elements: Array) => { this.dispatchAction({elements, type: 'UpdateElements'}); }); }); @@ -519,8 +623,8 @@ export default class Layout extends SonarPlugin { onAXElementSelected = debounce((key: ElementID) => { this.dispatchAction({key, type: 'SelectAXElement'}); this.client.send('setHighlighted', {id: key}); - this.getNodes([key], true).then((elements: Array) => { - this.dispatchAction({elements, type: 'UpdateElements'}); + this.getNodes([key], true, true).then((elements: Array) => { + this.dispatchAction({elements, type: 'UpdateAXElements'}); }); }); @@ -530,7 +634,7 @@ export default class Layout extends SonarPlugin { onDataValueChanged = (path: Array, value: any) => { const selected = this.state.inAXMode - ? this.state.selectedAX + ? this.state.AXselected : this.state.selected; this.client.send('setData', {id: selected, path, value}); this.props.logger.track('usage', 'layout:value-changed', { @@ -541,32 +645,41 @@ export default class Layout extends SonarPlugin { }; renderSidebar = () => { - return this.state.selected != null ? ( - - ) : null; - }; - - renderAXSidebar = () => { - return this.state.selectedAX != null ? ( - - ) : null; + if (this.state.inAXMode) { + // empty if no element selected w/in AX node tree + return ( + this.state.AXselected && ( + + ) + ); + } else { + // empty if no element selected w/in view tree + return ( + this.state.selected != null && ( + + ) + ); + } }; render() { const { initialised, + AXinitialised, selected, - selectedAX, + AXselected, root, + AXroot, elements, + AXelements, isSearchActive, inAXMode, outstandingSearchQuery, @@ -576,12 +689,12 @@ export default class Layout extends SonarPlugin { ); const AXButtonVisible = AXInspector !== null; @@ -590,7 +703,7 @@ export default class Layout extends SonarPlugin { @@ -606,7 +719,7 @@ export default class Layout extends SonarPlugin { {AXButtonVisible ? ( @@ -648,11 +761,10 @@ export default class Layout extends SonarPlugin { )} - {initialised && inAXMode ? AXInspector : null} + {AXinitialised && inAXMode ? : null} + {AXinitialised && inAXMode ? AXInspector : null} - - {inAXMode ? this.renderAXSidebar() : this.renderSidebar()} - + {this.renderSidebar()} ); } diff --git a/src/ui/components/VerticalRule.js b/src/ui/components/VerticalRule.js new file mode 100644 index 000000000..695661da3 --- /dev/null +++ b/src/ui/components/VerticalRule.js @@ -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', +}); diff --git a/src/ui/index.js b/src/ui/index.js index 9d01b0ec1..494b96dce 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -128,6 +128,7 @@ export {default as ResizeSensor} from './components/ResizeSensor.js'; // typhography 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 Heading} from './components/Heading.js'; @@ -154,6 +155,7 @@ export type { Element, ElementSearchResultSet, } from './components/elements-inspector/ElementsInspector.js'; +export {Elements} from './components/elements-inspector/elements.js'; export { default as ElementsInspector, } from './components/elements-inspector/ElementsInspector.js';