add litho accessibility rendering to Flipper accessibility mode [1/2]
Summary:
Litho renders differently based on whether applicable accessibility services are enabled. In Flipper's accessibility mode this will be forced (with the option to turn it off) so that you don't have to be running an accessibility service to actually see what someone running an accessibility service would.
Here's an example video of what the re-rendering does (this also happens on toggle of accessibility mode, this is just the settings option to force it):
{F137856647}
Reviewed By: jknoxville
Differential Revision: D9667222
fbshipit-source-id: 292353d89f07734f1e525f795b1d7daf4130e203
This commit is contained in:
committed by
Facebook Github Bot
parent
07650d0627
commit
73759e71db
@@ -32,6 +32,7 @@ import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Stack;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
public class InspectorSonarPlugin implements SonarPlugin {
|
public class InspectorSonarPlugin implements SonarPlugin {
|
||||||
@@ -44,6 +45,7 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
private TouchOverlayView mTouchOverlay;
|
private TouchOverlayView mTouchOverlay;
|
||||||
private SonarConnection mConnection;
|
private SonarConnection mConnection;
|
||||||
private @Nullable List<ExtensionCommand> mExtensionCommands;
|
private @Nullable List<ExtensionCommand> mExtensionCommands;
|
||||||
|
private boolean mShowLithoAccessibilitySettings;
|
||||||
|
|
||||||
/** An interface for extensions to the Inspector Sonar plugin */
|
/** An interface for extensions to the Inspector Sonar plugin */
|
||||||
public interface ExtensionCommand {
|
public interface ExtensionCommand {
|
||||||
@@ -110,6 +112,7 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
mApplication = wrapper;
|
mApplication = wrapper;
|
||||||
mScriptingEnvironment = scriptingEnvironment;
|
mScriptingEnvironment = scriptingEnvironment;
|
||||||
mExtensionCommands = extensions;
|
mExtensionCommands = extensions;
|
||||||
|
mShowLithoAccessibilitySettings = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -142,9 +145,13 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
connection.receive("getAXRoot", mGetAXRoot);
|
connection.receive("getAXRoot", mGetAXRoot);
|
||||||
connection.receive("getAXNodes", mGetAXNodes);
|
connection.receive("getAXNodes", mGetAXNodes);
|
||||||
connection.receive("onRequestAXFocus", mOnRequestAXFocus);
|
connection.receive("onRequestAXFocus", mOnRequestAXFocus);
|
||||||
|
connection.receive("shouldShowLithoAccessibilitySettings", mShouldShowLithoAccessibilitySettings);
|
||||||
|
|
||||||
if (mExtensionCommands != null) {
|
if (mExtensionCommands != null) {
|
||||||
for (ExtensionCommand extensionCommand : mExtensionCommands) {
|
for (ExtensionCommand extensionCommand : mExtensionCommands) {
|
||||||
|
if (extensionCommand.command().equals("forceLithoAXRender")) {
|
||||||
|
mShowLithoAccessibilitySettings = true;
|
||||||
|
}
|
||||||
connection.receive(
|
connection.receive(
|
||||||
extensionCommand.command(), extensionCommand.receiver(mObjectTracker, mConnection));
|
extensionCommand.command(), extensionCommand.receiver(mObjectTracker, mConnection));
|
||||||
}
|
}
|
||||||
@@ -166,6 +173,15 @@ public class InspectorSonarPlugin implements SonarPlugin {
|
|||||||
mConnection = null;
|
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 =
|
final SonarReceiver mGetRoot =
|
||||||
new MainThreadSonarReceiver(mConnection) {
|
new MainThreadSonarReceiver(mConnection) {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -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<ViewGroup> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
SearchIcon,
|
SearchIcon,
|
||||||
SonarSidebar,
|
SonarSidebar,
|
||||||
VerticalRule,
|
VerticalRule,
|
||||||
|
Popover,
|
||||||
|
ToggleButton,
|
||||||
} from 'sonar';
|
} from 'sonar';
|
||||||
|
|
||||||
import type {TrackType} from '../../fb-stubs/Logger.js';
|
import type {TrackType} from '../../fb-stubs/Logger.js';
|
||||||
@@ -45,7 +47,11 @@ export type InspectorState = {|
|
|||||||
AXroot: ?ElementID,
|
AXroot: ?ElementID,
|
||||||
AXelements: {[key: ElementID]: Element},
|
AXelements: {[key: ElementID]: Element},
|
||||||
inAXMode: boolean,
|
inAXMode: boolean,
|
||||||
|
forceLithoAXRender: boolean,
|
||||||
AXtoNonAXMapping: {[key: ElementID]: ElementID},
|
AXtoNonAXMapping: {[key: ElementID]: ElementID},
|
||||||
|
accessibilitySettingsOpen: boolean,
|
||||||
|
showLithoAccessibilitySettings: boolean,
|
||||||
|
//
|
||||||
isAlignmentMode: boolean,
|
isAlignmentMode: boolean,
|
||||||
logCounter: number,
|
logCounter: number,
|
||||||
|};
|
|};
|
||||||
@@ -121,6 +127,18 @@ const SearchIconContainer = styled('div')({
|
|||||||
marginRight: 9,
|
marginRight: 9,
|
||||||
marginTop: -3,
|
marginTop: -3,
|
||||||
marginLeft: 4,
|
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<
|
class LayoutSearchInput extends Component<
|
||||||
@@ -183,12 +201,16 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
outstandingSearchQuery: null,
|
outstandingSearchQuery: null,
|
||||||
// properties for ax mode
|
// properties for ax mode
|
||||||
inAXMode: false,
|
inAXMode: false,
|
||||||
|
forceLithoAXRender: true,
|
||||||
AXelements: {},
|
AXelements: {},
|
||||||
AXinitialised: false,
|
AXinitialised: false,
|
||||||
AXroot: null,
|
AXroot: null,
|
||||||
AXselected: null,
|
AXselected: null,
|
||||||
AXfocused: null,
|
AXfocused: null,
|
||||||
|
accessibilitySettingsOpen: false,
|
||||||
AXtoNonAXMapping: {},
|
AXtoNonAXMapping: {},
|
||||||
|
showLithoAccessibilitySettings: false,
|
||||||
|
//
|
||||||
isAlignmentMode: false,
|
isAlignmentMode: false,
|
||||||
logCounter: 0,
|
logCounter: 0,
|
||||||
};
|
};
|
||||||
@@ -327,6 +349,20 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
SetAXMode(state: InspectorState, {inAXMode}: {inAXMode: boolean}) {
|
SetAXMode(state: InspectorState, {inAXMode}: {inAXMode: boolean}) {
|
||||||
return {inAXMode};
|
return {inAXMode};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
SetLithoRenderMode(
|
||||||
|
state: InspectorState,
|
||||||
|
{forceLithoAXRender}: {forceLithoAXRender: boolean},
|
||||||
|
) {
|
||||||
|
return {forceLithoAXRender};
|
||||||
|
},
|
||||||
|
|
||||||
|
SetAccessibilitySettingsOpen(
|
||||||
|
state: InspectorState,
|
||||||
|
{accessibilitySettingsOpen}: {accessibilitySettingsOpen: boolean},
|
||||||
|
) {
|
||||||
|
return {accessibilitySettingsOpen};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
search(query: string) {
|
search(query: string) {
|
||||||
@@ -509,6 +545,15 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initAX() {
|
initAX() {
|
||||||
|
// TODO: uncomment once Litho open source updates
|
||||||
|
// this.client
|
||||||
|
// .call('shouldShowLithoAccessibilitySettings')
|
||||||
|
// .then((showLithoAccessibilitySettings: boolean) => {
|
||||||
|
// this.setState({
|
||||||
|
// showLithoAccessibilitySettings,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
performance.mark('InitAXRoot');
|
performance.mark('InitAXRoot');
|
||||||
this.client.call('getAXRoot').then((element: Element) => {
|
this.client.call('getAXRoot').then((element: Element) => {
|
||||||
this.dispatchAction({elements: [element], type: 'UpdateAXElements'});
|
this.dispatchAction({elements: [element], type: 'UpdateAXElements'});
|
||||||
@@ -835,8 +880,48 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
|
|
||||||
onToggleAccessibility = () => {
|
onToggleAccessibility = () => {
|
||||||
const inAXMode = !this.state.inAXMode;
|
const inAXMode = !this.state.inAXMode;
|
||||||
|
const {
|
||||||
|
forceLithoAXRender,
|
||||||
|
AXroot,
|
||||||
|
showLithoAccessibilitySettings,
|
||||||
|
} = this.state;
|
||||||
this.props.logger.track('usage', 'accessibility:modeToggled', {inAXMode});
|
this.props.logger.track('usage', 'accessibility:modeToggled', {inAXMode});
|
||||||
this.dispatchAction({inAXMode, type: 'SetAXMode'});
|
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 = () => {
|
onToggleAlignment = () => {
|
||||||
@@ -998,6 +1083,22 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getAccessibilitySettingsPopover(forceLithoAXRender: boolean) {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
onDismiss={this.onCloseAccessibilitySettings}
|
||||||
|
forceOpts={{skewLeft: true, minWidth: 280}}>
|
||||||
|
<SettingsItem>
|
||||||
|
<ToggleButton
|
||||||
|
onClick={this.onToggleForceLithoAXRender}
|
||||||
|
toggled={forceLithoAXRender}
|
||||||
|
/>
|
||||||
|
<SettingsLabel>Force Litho Accessibility Rendering</SettingsLabel>
|
||||||
|
</SettingsItem>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
initialised,
|
initialised,
|
||||||
@@ -1011,8 +1112,11 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
AXelements,
|
AXelements,
|
||||||
isSearchActive,
|
isSearchActive,
|
||||||
inAXMode,
|
inAXMode,
|
||||||
|
forceLithoAXRender,
|
||||||
outstandingSearchQuery,
|
outstandingSearchQuery,
|
||||||
isAlignmentMode,
|
isAlignmentMode,
|
||||||
|
accessibilitySettingsOpen,
|
||||||
|
showLithoAccessibilitySettings,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1074,6 +1178,24 @@ export default class Layout extends SonarPlugin<InspectorState> {
|
|||||||
<LayoutSearchInput onSubmit={this.search.bind(this)} />
|
<LayoutSearchInput onSubmit={this.search.bind(this)} />
|
||||||
{outstandingSearchQuery && <LoadingSpinner size={16} />}
|
{outstandingSearchQuery && <LoadingSpinner size={16} />}
|
||||||
</SearchBox>
|
</SearchBox>
|
||||||
|
{inAXMode &&
|
||||||
|
showLithoAccessibilitySettings && (
|
||||||
|
<SearchIconContainer
|
||||||
|
onClick={this.onOpenAccessibilitySettings}
|
||||||
|
role="button">
|
||||||
|
<Glyph
|
||||||
|
name="settings"
|
||||||
|
size={16}
|
||||||
|
color={
|
||||||
|
accessibilitySettingsOpen
|
||||||
|
? colors.macOSTitleBarIconSelected
|
||||||
|
: colors.macOSTitleBarIconActive
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{accessibilitySettingsOpen &&
|
||||||
|
this.getAccessibilitySettingsPopover(forceLithoAXRender)}
|
||||||
|
</SearchIconContainer>
|
||||||
|
)}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<FlexRow fill={true}>
|
<FlexRow fill={true}>
|
||||||
{initialised ? (
|
{initialised ? (
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const Anchor = styled('img')({
|
|||||||
transform: 'translate(-50%, calc(100% + 2px))',
|
transform: 'translate(-50%, calc(100% + 2px))',
|
||||||
});
|
});
|
||||||
|
|
||||||
const PopoverContainer = styled(FlexColumn)({
|
const PopoverContainer = styled(FlexColumn)(props => ({
|
||||||
backgroundColor: colors.white,
|
backgroundColor: colors.white,
|
||||||
borderRadius: 7,
|
borderRadius: 7,
|
||||||
border: '1px solid rgba(0,0,0,0.3)',
|
border: '1px solid rgba(0,0,0,0.3)',
|
||||||
@@ -28,24 +28,30 @@ const PopoverContainer = styled(FlexColumn)({
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
marginTop: 15,
|
marginTop: 15,
|
||||||
left: '50%',
|
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',
|
overflow: 'hidden',
|
||||||
'&::before': {
|
'&::before': {
|
||||||
content: '""',
|
content: '""',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: props.opts.skewLeft
|
||||||
|
? 'translateX(calc(-100% + 22px))'
|
||||||
|
: 'translateX(-50%)',
|
||||||
height: 13,
|
height: 13,
|
||||||
top: -13,
|
top: -13,
|
||||||
width: 26,
|
width: 26,
|
||||||
backgroundColor: colors.white,
|
backgroundColor: colors.white,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
type Props = {|
|
type Props = {|
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
onDismiss: Function,
|
onDismiss: Function,
|
||||||
|
forceOpts?: Object,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export default class Popover extends PureComponent<Props> {
|
export default class Popover extends PureComponent<Props> {
|
||||||
@@ -80,8 +86,15 @@ export default class Popover extends PureComponent<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return [
|
return [
|
||||||
<Anchor src="./anchor.svg" key="anchor" />,
|
<Anchor
|
||||||
<PopoverContainer innerRef={this._setRef} key="popup">
|
src="./anchor.svg"
|
||||||
|
key="anchor"
|
||||||
|
opts={this.props.forceOpts || {}}
|
||||||
|
/>,
|
||||||
|
<PopoverContainer
|
||||||
|
innerRef={this._setRef}
|
||||||
|
key="popup"
|
||||||
|
opts={this.props.forceOpts || {}}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</PopoverContainer>,
|
</PopoverContainer>,
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user