diff --git a/packages/react-native/Libraries/Pressability/PressabilityDebug.js b/packages/react-native/Libraries/Pressability/PressabilityDebug.js
index 90b83e49e51c..842844133f4d 100644
--- a/packages/react-native/Libraries/Pressability/PressabilityDebug.js
+++ b/packages/react-native/Libraries/Pressability/PressabilityDebug.js
@@ -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;
@@ -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__) {
@@ -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;
diff --git a/packages/react-native/Libraries/Pressability/__tests__/PressabilityDebug-itest.js b/packages/react-native/Libraries/Pressability/__tests__/PressabilityDebug-itest.js
new file mode 100644
index 000000000000..25bc6a3aa1e5
--- /dev/null
+++ b/packages/react-native/Libraries/Pressability/__tests__/PressabilityDebug-itest.js
@@ -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 ;
+ }
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(root.getRenderedOutput({props: []}).toJSX()).toEqual();
+
+ Fantom.runTask(() => {
+ PressabilityDebug.setEnabled(true);
+ });
+
+ expect(root.getRenderedOutput({props: []}).toJSX()).toEqual(
+
+
+ ,
+ );
+
+ Fantom.runTask(() => {
+ PressabilityDebug.setEnabled(false);
+ });
+
+ expect(root.getRenderedOutput({props: []}).toJSX()).toEqual();
+ expect(mountCount).toBe(1);
+ });
+
+ it('colors pressable text when enabled', () => {
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render( {}}>text);
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['foregroundColor']}).toJSX(),
+ ).toEqual(
+ text,
+ );
+
+ Fantom.runTask(() => {
+ PressabilityDebug.setEnabled(true);
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['foregroundColor']}).toJSX(),
+ ).toEqual(
+ text,
+ );
+ });
+
+ it('colors nested pressable text when enabled', () => {
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render(
+
+ {}}>nested
+ ,
+ );
+ });
+
+ Fantom.runTask(() => {
+ PressabilityDebug.setEnabled(true);
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['foregroundColor']}).toJSX(),
+ ).toEqual(
+
+ nested
+ ,
+ );
+ });
+
+ it('does not color disabled text', () => {
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ PressabilityDebug.setEnabled(true);
+ });
+
+ Fantom.runTask(() => {
+ root.render(
+ {}}>
+ text
+ ,
+ );
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['foregroundColor']}).toJSX(),
+ ).toEqual(
+ text,
+ );
+ });
+});
diff --git a/packages/react-native/Libraries/ReactNative/AppContainer-dev.js b/packages/react-native/Libraries/ReactNative/AppContainer-dev.js
index a0abc38caec4..a9457cf09b36 100644
--- a/packages/react-native/Libraries/ReactNative/AppContainer-dev.js
+++ b/packages/react-native/Libraries/ReactNative/AppContainer-dev.js
@@ -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__;
@@ -51,7 +51,6 @@ type ExternalInspection = {
type InspectorDeferredProps = {
inspectedViewRef: InspectedViewRef,
- onInspectedViewRerenderRequest: () => void,
reactDevToolsAgent?: ReactDevToolsAgent,
devMenuInspectorOpen: boolean,
externalInspection: ExternalInspection,
@@ -59,7 +58,6 @@ type InspectorDeferredProps = {
const InspectorDeferred = ({
inspectedViewRef,
- onInspectedViewRerenderRequest,
reactDevToolsAgent,
devMenuInspectorOpen,
externalInspection,
@@ -72,7 +70,6 @@ const InspectorDeferred = ({
return (
(reactDevToolsHook?.reactDevtoolsAgent);
@@ -157,7 +153,6 @@ const AppContainer = ({
{children}
@@ -172,11 +167,6 @@ const AppContainer = ({
);
}
- const onInspectedViewRerenderRequest = useCallback(
- () => setKey(k => k + 1),
- [],
- );
-
return (
= 0)) {
@@ -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.
*
@@ -485,10 +498,15 @@ component PressableVirtualText(
const [isHighlighted, eventHandlersForText] = useTextPressability(
textPressabilityProps,
);
+ const style = usePressabilityDebugStyle(
+ textProps.style,
+ textPressabilityProps.onPress,
+ );
return (
void,
reactDevToolsAgent?: ReactDevToolsAgent,
devMenuInspectorOpen: boolean,
externalInspection: ExternalInspection,
@@ -64,7 +63,6 @@ type Props = {
function Inspector({
inspectedViewRef,
- onRequestRerenderApp,
reactDevToolsAgent,
devMenuInspectorOpen,
externalInspection,
@@ -79,6 +77,7 @@ function Inspector({
const [selectionIndex, setSelectionIndex] = useState(null);
const [elementsHierarchy, setElementsHierarchy] =
useState(null);
+ const touchTargeting = PressabilityDebug.useIsEnabled();
// Derive inspecting state: external inspection forces it on, otherwise use local state
const isInspecting = externalInspectingEnabled || inspectingEnabled;
@@ -160,7 +159,6 @@ function Inspector({
const setTouchTargeting = (val: boolean) => {
PressabilityDebug.setEnabled(val);
- onRequestRerenderApp();
};
const panelContainerStyle =
@@ -188,7 +186,7 @@ function Inspector({
hierarchy={elementsHierarchy}
selection={selectionIndex}
setSelection={setSelection}
- touchTargeting={PressabilityDebug.isEnabled()}
+ touchTargeting={touchTargeting}
setTouchTargeting={setTouchTargeting}
/>