diff --git a/android/src/main/java/com/facebook/sonar/plugins/inspector/InspectorSonarPlugin.java b/android/src/main/java/com/facebook/sonar/plugins/inspector/InspectorSonarPlugin.java index 384b7c713..29550171a 100644 --- a/android/src/main/java/com/facebook/sonar/plugins/inspector/InspectorSonarPlugin.java +++ b/android/src/main/java/com/facebook/sonar/plugins/inspector/InspectorSonarPlugin.java @@ -32,6 +32,7 @@ import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil; import java.util.ArrayList; import java.util.List; +import java.util.Stack; import javax.annotation.Nullable; public class InspectorSonarPlugin implements SonarPlugin { @@ -44,6 +45,7 @@ public class InspectorSonarPlugin implements SonarPlugin { private TouchOverlayView mTouchOverlay; private SonarConnection mConnection; private @Nullable List mExtensionCommands; + private boolean mShowLithoAccessibilitySettings; /** An interface for extensions to the Inspector Sonar plugin */ public interface ExtensionCommand { @@ -110,6 +112,7 @@ public class InspectorSonarPlugin implements SonarPlugin { mApplication = wrapper; mScriptingEnvironment = scriptingEnvironment; mExtensionCommands = extensions; + mShowLithoAccessibilitySettings = false; } @Override @@ -142,9 +145,13 @@ public class InspectorSonarPlugin implements SonarPlugin { connection.receive("getAXRoot", mGetAXRoot); connection.receive("getAXNodes", mGetAXNodes); connection.receive("onRequestAXFocus", mOnRequestAXFocus); + connection.receive("shouldShowLithoAccessibilitySettings", mShouldShowLithoAccessibilitySettings); if (mExtensionCommands != null) { for (ExtensionCommand extensionCommand : mExtensionCommands) { + if (extensionCommand.command().equals("forceLithoAXRender")) { + mShowLithoAccessibilitySettings = true; + } connection.receive( extensionCommand.command(), extensionCommand.receiver(mObjectTracker, mConnection)); } @@ -166,6 +173,15 @@ public class InspectorSonarPlugin implements SonarPlugin { mConnection = null; } + final SonarReceiver mShouldShowLithoAccessibilitySettings = + new MainThreadSonarReceiver(mConnection) { + @Override + public void onReceiveOnMainThread(SonarObject params, SonarResponder responder) + throws Exception { + responder.success(new SonarObject.Builder().put("showLithoAccessibilitySettings", mShowLithoAccessibilitySettings).build()); + } + }; + final SonarReceiver mGetRoot = new MainThreadSonarReceiver(mConnection) { @Override diff --git a/android/src/main/java/com/facebook/sonar/plugins/litho/GenerateLithoAccessibilityRenderExtensionCommand.java b/android/src/main/java/com/facebook/sonar/plugins/litho/GenerateLithoAccessibilityRenderExtensionCommand.java new file mode 100644 index 000000000..08d2f7f64 --- /dev/null +++ b/android/src/main/java/com/facebook/sonar/plugins/litho/GenerateLithoAccessibilityRenderExtensionCommand.java @@ -0,0 +1,84 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.sonar.plugins.litho; + +import android.view.View; +import android.view.ViewGroup; +import com.facebook.litho.LithoView; +import com.facebook.sonar.core.SonarConnection; +import com.facebook.sonar.core.SonarObject; +import com.facebook.sonar.core.SonarReceiver; +import com.facebook.sonar.core.SonarResponder; +import com.facebook.sonar.plugins.common.MainThreadSonarReceiver; +import com.facebook.sonar.plugins.inspector.ApplicationWrapper; +import com.facebook.sonar.plugins.inspector.InspectorSonarPlugin; +import com.facebook.sonar.plugins.inspector.ObjectTracker; + +import java.util.Stack; + +public final class GenerateLithoAccessibilityRenderExtensionCommand implements InspectorSonarPlugin.ExtensionCommand { + + @Override + public String command() { + return "forceLithoAXRender"; + } + + @Override + public SonarReceiver receiver(final ObjectTracker tracker, final SonarConnection connection) { + return new MainThreadSonarReceiver(connection) { + @Override + public void onReceiveOnMainThread(final SonarObject params, final SonarResponder responder) + throws Exception { + final String applicationId = params.getString("applicationId"); + + // check that the application is valid + if (applicationId == null) { + return; + } + final Object obj = tracker.get(applicationId); + if (obj != null && !(obj instanceof ApplicationWrapper)) { + return; + } + + final ApplicationWrapper applicationWrapper = ((ApplicationWrapper) obj); + final boolean forceLithoAXRender = params.getBoolean("forceLithoAXRender"); + final boolean prevForceLithoAXRender = Boolean.getBoolean("is_accessibility_enabled"); + + // nothing has changed, so return + if (forceLithoAXRender == prevForceLithoAXRender) { + return; + } + + // change property and rerender + System.setProperty("is_accessibility_enabled", forceLithoAXRender + ""); + forceRerenderAllLithoViews(forceLithoAXRender, applicationWrapper); + } + }; + } + + private void forceRerenderAllLithoViews(boolean forceLithoAXRender, ApplicationWrapper applicationWrapper) { + + // iterate through tree and rerender all litho views + Stack lithoViewSearchStack = new Stack<>(); + for (View root : applicationWrapper.getViewRoots()) { + if (root instanceof ViewGroup) { + lithoViewSearchStack.push((ViewGroup) root); + } + } + + while (!lithoViewSearchStack.isEmpty()) { + ViewGroup v = lithoViewSearchStack.pop(); + if (v instanceof LithoView) { + // TODO: uncomment once Litho open source updates +// ((LithoView) v).rerenderForAccessibility(forceLithoAXRender); + } else { + for (int i = 0; i < v.getChildCount(); i++) { + View child = v.getChildAt(i); + if (child instanceof ViewGroup) { + lithoViewSearchStack.push((ViewGroup) child); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/plugins/layout/index.js b/src/plugins/layout/index.js index 7ce17aa36..40c9fb7da 100644 --- a/src/plugins/layout/index.js +++ b/src/plugins/layout/index.js @@ -23,6 +23,8 @@ import { SearchIcon, SonarSidebar, VerticalRule, + Popover, + ToggleButton, } from 'sonar'; import type {TrackType} from '../../fb-stubs/Logger.js'; @@ -45,7 +47,11 @@ export type InspectorState = {| AXroot: ?ElementID, AXelements: {[key: ElementID]: Element}, inAXMode: boolean, + forceLithoAXRender: boolean, AXtoNonAXMapping: {[key: ElementID]: ElementID}, + accessibilitySettingsOpen: boolean, + showLithoAccessibilitySettings: boolean, + // isAlignmentMode: boolean, logCounter: number, |}; @@ -121,6 +127,18 @@ const SearchIconContainer = styled('div')({ marginRight: 9, marginTop: -3, marginLeft: 4, + position: 'relative', // for settings popover positioning +}); + +const SettingsItem = styled('div')({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +const SettingsLabel = styled('div')({ + marginLeft: 5, + marginRight: 15, }); class LayoutSearchInput extends Component< @@ -183,12 +201,16 @@ export default class Layout extends SonarPlugin { outstandingSearchQuery: null, // properties for ax mode inAXMode: false, + forceLithoAXRender: true, AXelements: {}, AXinitialised: false, AXroot: null, AXselected: null, AXfocused: null, + accessibilitySettingsOpen: false, AXtoNonAXMapping: {}, + showLithoAccessibilitySettings: false, + // isAlignmentMode: false, logCounter: 0, }; @@ -327,6 +349,20 @@ export default class Layout extends SonarPlugin { SetAXMode(state: InspectorState, {inAXMode}: {inAXMode: boolean}) { return {inAXMode}; }, + + SetLithoRenderMode( + state: InspectorState, + {forceLithoAXRender}: {forceLithoAXRender: boolean}, + ) { + return {forceLithoAXRender}; + }, + + SetAccessibilitySettingsOpen( + state: InspectorState, + {accessibilitySettingsOpen}: {accessibilitySettingsOpen: boolean}, + ) { + return {accessibilitySettingsOpen}; + }, }; search(query: string) { @@ -509,6 +545,15 @@ export default class Layout extends SonarPlugin { } initAX() { + // TODO: uncomment once Litho open source updates + // this.client + // .call('shouldShowLithoAccessibilitySettings') + // .then((showLithoAccessibilitySettings: boolean) => { + // this.setState({ + // showLithoAccessibilitySettings, + // }); + // }); + performance.mark('InitAXRoot'); this.client.call('getAXRoot').then((element: Element) => { this.dispatchAction({elements: [element], type: 'UpdateAXElements'}); @@ -835,8 +880,48 @@ export default class Layout extends SonarPlugin { onToggleAccessibility = () => { const inAXMode = !this.state.inAXMode; + const { + forceLithoAXRender, + AXroot, + showLithoAccessibilitySettings, + } = this.state; this.props.logger.track('usage', 'accessibility:modeToggled', {inAXMode}); this.dispatchAction({inAXMode, type: 'SetAXMode'}); + + // only force render if litho accessibility is included in app + if (showLithoAccessibilitySettings) { + this.client.send('forceLithoAXRender', { + forceLithoAXRender: inAXMode && forceLithoAXRender, + applicationId: AXroot, + }); + } + }; + + onToggleForceLithoAXRender = () => { + // only force render if litho accessibility is included in app + if (this.state.showLithoAccessibilitySettings) { + const forceLithoAXRender = !this.state.forceLithoAXRender; + const applicationId = this.state.AXroot; + this.dispatchAction({forceLithoAXRender, type: 'SetLithoRenderMode'}); + this.client.send('forceLithoAXRender', { + forceLithoAXRender: forceLithoAXRender, + applicationId, + }); + } + }; + + onOpenAccessibilitySettings = () => { + this.dispatchAction({ + accessibilitySettingsOpen: true, + type: 'SetAccessibilitySettingsOpen', + }); + }; + + onCloseAccessibilitySettings = () => { + this.dispatchAction({ + accessibilitySettingsOpen: false, + type: 'SetAccessibilitySettingsOpen', + }); }; onToggleAlignment = () => { @@ -998,6 +1083,22 @@ export default class Layout extends SonarPlugin { } }; + getAccessibilitySettingsPopover(forceLithoAXRender: boolean) { + return ( + + + + Force Litho Accessibility Rendering + + + ); + } + render() { const { initialised, @@ -1011,8 +1112,11 @@ export default class Layout extends SonarPlugin { AXelements, isSearchActive, inAXMode, + forceLithoAXRender, outstandingSearchQuery, isAlignmentMode, + accessibilitySettingsOpen, + showLithoAccessibilitySettings, } = this.state; return ( @@ -1074,6 +1178,24 @@ export default class Layout extends SonarPlugin { {outstandingSearchQuery && } + {inAXMode && + showLithoAccessibilitySettings && ( + + + {accessibilitySettingsOpen && + this.getAccessibilitySettingsPopover(forceLithoAXRender)} + + )} {initialised ? ( diff --git a/src/ui/components/Popover.js b/src/ui/components/Popover.js index 81cce75ad..be60cf252 100644 --- a/src/ui/components/Popover.js +++ b/src/ui/components/Popover.js @@ -18,7 +18,7 @@ const Anchor = styled('img')({ transform: 'translate(-50%, calc(100% + 2px))', }); -const PopoverContainer = styled(FlexColumn)({ +const PopoverContainer = styled(FlexColumn)(props => ({ backgroundColor: colors.white, borderRadius: 7, border: '1px solid rgba(0,0,0,0.3)', @@ -28,24 +28,30 @@ const PopoverContainer = styled(FlexColumn)({ bottom: 0, marginTop: 15, left: '50%', - transform: 'translate(-50%, calc(100% + 15px))', + minWidth: props.opts.minWidth || 'auto', + transform: props.opts.skewLeft + ? 'translate(calc(-100% + 22px), calc(100% + 15px))' + : 'translate(-50%, calc(100% + 15px))', overflow: 'hidden', '&::before': { content: '""', display: 'block', position: 'absolute', left: '50%', - transform: 'translateX(-50%)', + transform: props.opts.skewLeft + ? 'translateX(calc(-100% + 22px))' + : 'translateX(-50%)', height: 13, top: -13, width: 26, backgroundColor: colors.white, }, -}); +})); type Props = {| children: React.Node, onDismiss: Function, + forceOpts?: Object, |}; export default class Popover extends PureComponent { @@ -80,8 +86,15 @@ export default class Popover extends PureComponent { render() { return [ - , - + , + {this.props.children} , ];