Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ type Props = Readonly<{
*
*/
export function PressabilityDebugView(props: Props): React.Node {
const enabled = useIsEnabled();
if (__DEV__) {
if (isEnabled()) {
if (enabled) {
const normalizedColor = normalizeColor(props.color);
if (typeof normalizedColor !== 'number') {
return null;
Expand Down Expand Up @@ -70,6 +71,7 @@ export function PressabilityDebugView(props: Props): React.Node {
}

let isDebugEnabled = false;
const listeners: Set<() => void> = new Set();

export function isEnabled(): boolean {
if (__DEV__) {
Expand All @@ -80,6 +82,30 @@ export function isEnabled(): boolean {

export function setEnabled(value: boolean): void {
if (__DEV__) {
if (isDebugEnabled === value) {
return;
}
isDebugEnabled = value;
listeners.forEach(listener => listener());
}
}

function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}

function useIsEnabledDev(): boolean {
return React.useSyncExternalStore(subscribe, isEnabled);
}

/**
* Like `isEnabled`, but re-renders the calling component when the value
* changes, so toggling it does not require remounting the app. Outside of
* `__DEV__` it always returns `false` and uses no hooks.
*/
export const useIsEnabled: () => boolean = __DEV__
? useIsEnabledDev
: () => false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/

import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';

import * as PressabilityDebug from '../PressabilityDebug';
import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {useEffect} from 'react';
import {Pressable, Text} from 'react-native';

describe('PressabilityDebug', () => {
beforeEach(() => {
Fantom.runTask(() => {
PressabilityDebug.setEnabled(false);
});
});

afterEach(() => {
Fantom.runTask(() => {
PressabilityDebug.setEnabled(false);
});
});

it('shows press targets when enabled, without remounting', () => {
const root = Fantom.createRoot();
let mountCount = 0;

function Screen() {
useEffect(() => {
mountCount++;
}, []);
return <Pressable style={{height: 10}} />;
}

Fantom.runTask(() => {
root.render(<Screen />);
});

expect(root.getRenderedOutput({props: []}).toJSX()).toEqual(<rn-view />);

Fantom.runTask(() => {
PressabilityDebug.setEnabled(true);
});

expect(root.getRenderedOutput({props: []}).toJSX()).toEqual(
<rn-view>
<rn-view />
</rn-view>,
);

Fantom.runTask(() => {
PressabilityDebug.setEnabled(false);
});

expect(root.getRenderedOutput({props: []}).toJSX()).toEqual(<rn-view />);
expect(mountCount).toBe(1);
});

it('colors pressable text when enabled', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<Text onPress={() => {}}>text</Text>);
});

expect(
root.getRenderedOutput({props: ['foregroundColor']}).toJSX(),
).toEqual(
<rn-paragraph foregroundColor="rgba(0, 0, 0, 0)">text</rn-paragraph>,
);

Fantom.runTask(() => {
PressabilityDebug.setEnabled(true);
});

expect(
root.getRenderedOutput({props: ['foregroundColor']}).toJSX(),
).toEqual(
<rn-paragraph foregroundColor="rgba(255, 0, 255, 1)">text</rn-paragraph>,
);
});

it('colors nested pressable text when enabled', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(
<Text>
<Text onPress={() => {}}>nested</Text>
</Text>,
);
});

Fantom.runTask(() => {
PressabilityDebug.setEnabled(true);
});

expect(
root.getRenderedOutput({props: ['foregroundColor']}).toJSX(),
).toEqual(
<rn-paragraph foregroundColor="rgba(0, 0, 0, 0)">
<rn-text foregroundColor="rgba(255, 0, 255, 1)">nested</rn-text>
</rn-paragraph>,
);
});

it('does not color disabled text', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
PressabilityDebug.setEnabled(true);
});

Fantom.runTask(() => {
root.render(
<Text disabled onPress={() => {}}>
text
</Text>,
);
});

expect(
root.getRenderedOutput({props: ['foregroundColor']}).toJSX(),
).toEqual(
<rn-paragraph foregroundColor="rgba(0, 0, 0, 0)">text</rn-paragraph>,
);
});
});
13 changes: 1 addition & 12 deletions packages/react-native/Libraries/ReactNative/AppContainer-dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {RootTagContext, createRootTag} from './RootTag';
import * as React from 'react';
import {useRef} from 'react';

const {useEffect, useState, useCallback} = React;
const {useEffect, useState} = React;

const reactDevToolsHook: ReactDevToolsGlobalHook = (window as $FlowFixMe)
.__REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand All @@ -51,15 +51,13 @@ type ExternalInspection = {

type InspectorDeferredProps = {
inspectedViewRef: InspectedViewRef,
onInspectedViewRerenderRequest: () => void,
reactDevToolsAgent?: ReactDevToolsAgent,
devMenuInspectorOpen: boolean,
externalInspection: ExternalInspection,
};

const InspectorDeferred = ({
inspectedViewRef,
onInspectedViewRerenderRequest,
reactDevToolsAgent,
devMenuInspectorOpen,
externalInspection,
Expand All @@ -72,7 +70,6 @@ const InspectorDeferred = ({
return (
<Inspector
inspectedViewRef={inspectedViewRef}
onRequestRerenderApp={onInspectedViewRerenderRequest}
reactDevToolsAgent={reactDevToolsAgent}
devMenuInspectorOpen={devMenuInspectorOpen}
externalInspection={externalInspection}
Expand Down Expand Up @@ -118,7 +115,6 @@ const AppContainer = ({
debuggingOverlayRef,
);

const [key, setKey] = useState(0);
const [shouldRenderInspector, setShouldRenderInspector] = useState(false);
const [reactDevToolsAgent, setReactDevToolsAgent] =
useState<ReactDevToolsAgent | void>(reactDevToolsHook?.reactDevtoolsAgent);
Expand Down Expand Up @@ -157,7 +153,6 @@ const AppContainer = ({
<View
collapsable={reactDevToolsAgent == null && !shouldRenderInspector}
pointerEvents="box-none"
key={key}
style={rootViewStyle || styles.container}
ref={innerViewRef}>
{children}
Expand All @@ -172,11 +167,6 @@ const AppContainer = ({
);
}

const onInspectedViewRerenderRequest = useCallback(
() => setKey(k => k + 1),
[],
);

return (
<RootTagContext.Provider value={createRootTag(rootTag)}>
<View
Expand All @@ -198,7 +188,6 @@ const AppContainer = ({
externalInspection.externalInspectingEnabled) && (
<InspectorDeferred
inspectedViewRef={innerViewRef}
onInspectedViewRerenderRequest={onInspectedViewRerenderRequest}
reactDevToolsAgent={reactDevToolsAgent}
devMenuInspectorOpen={shouldRenderInspector}
externalInspection={externalInspection}
Expand Down
34 changes: 29 additions & 5 deletions packages/react-native/Libraries/Text/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,6 @@ const TextImpl: component(
selectionColor != null ? processColor(selectionColor) : undefined;

let _style = style;
if (__DEV__) {
if (PressabilityDebug.isEnabled() && onPress != null) {
_style = [style, {color: 'magenta'}];
}
}

let _numberOfLines = numberOfLines;
if (_numberOfLines != null && !(_numberOfLines >= 0)) {
Expand Down Expand Up @@ -471,6 +466,24 @@ function useTextPressability({
);
}

function usePressabilityDebugStyleDev(
style: ?TextStyleProp,
onPress: ?(event: GestureResponderEvent) => unknown,
): ?TextStyleProp {
const isDebugEnabled = PressabilityDebug.useIsEnabled();
return isDebugEnabled && onPress != null
? [style, {color: 'magenta'}]
: style;
}

/**
* Colors pressable text when press targets are shown by the Inspector.
* Outside of `__DEV__` it returns the style unchanged and uses no hooks.
*/
const usePressabilityDebugStyle: typeof usePressabilityDebugStyleDev = __DEV__
? usePressabilityDebugStyleDev
: style => style;

/**
* Wrap the NativeVirtualText component and initialize pressability.
*
Expand All @@ -485,10 +498,15 @@ component PressableVirtualText(
const [isHighlighted, eventHandlersForText] = useTextPressability(
textPressabilityProps,
);
const style = usePressabilityDebugStyle(
textProps.style,
textPressabilityProps.onPress,
);
Comment on lines +501 to +504

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we wrap this in DEV too? Or stub usePressabilityDebugStyle with a no-op outside of DEV?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, went with the stub. I also moved it up next to useTextPressability and used the named prop types instead


return (
<NativeVirtualText
{...textProps}
style={style}
{...eventHandlersForText}
isHighlighted={isHighlighted}
isPressable={true}
Expand All @@ -513,12 +531,18 @@ component PressableText(
textPressabilityProps,
);

const style = usePressabilityDebugStyle(
textProps.style,
textPressabilityProps.onPress,
);

const NativeComponent =
selectable === true ? NativeSelectableText : NativeText;

return (
<NativeComponent
{...textProps}
style={style}
{...eventHandlersForText}
isHighlighted={isHighlighted}
isPressable={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,13 @@ type ExternalInspection = {

type Props = {
inspectedViewRef: InspectedViewRef,
onRequestRerenderApp: () => void,
reactDevToolsAgent?: ReactDevToolsAgent,
devMenuInspectorOpen: boolean,
externalInspection: ExternalInspection,
};

function Inspector({
inspectedViewRef,
onRequestRerenderApp,
reactDevToolsAgent,
devMenuInspectorOpen,
externalInspection,
Expand All @@ -79,6 +77,7 @@ function Inspector({
const [selectionIndex, setSelectionIndex] = useState<?number>(null);
const [elementsHierarchy, setElementsHierarchy] =
useState<?ElementsHierarchy>(null);
const touchTargeting = PressabilityDebug.useIsEnabled();

// Derive inspecting state: external inspection forces it on, otherwise use local state
const isInspecting = externalInspectingEnabled || inspectingEnabled;
Expand Down Expand Up @@ -160,7 +159,6 @@ function Inspector({

const setTouchTargeting = (val: boolean) => {
PressabilityDebug.setEnabled(val);
onRequestRerenderApp();
};

const panelContainerStyle =
Expand Down Expand Up @@ -188,7 +186,7 @@ function Inspector({
hierarchy={elementsHierarchy}
selection={selectionIndex}
setSelection={setSelection}
touchTargeting={PressabilityDebug.isEnabled()}
touchTargeting={touchTargeting}
setTouchTargeting={setTouchTargeting}
/>
</SafeAreaView>
Expand Down
Loading