Skip to content

Commit 6530092

Browse files
Abbondanzometa-codesync[bot]
authored andcommitted
Add BackHandler to dismiss LogBox toasts on back press (facebook#56474)
Summary: Pull Request resolved: facebook#56474 On Android, pressing the hardware back button while LogBox notification toasts or the full inspector overlay are visible has no effect on the JS side. The only way to dismiss notifications is the on-screen X button, and the only way to close the inspector is via Minimize/Dismiss. This adds `BackHandler` listeners to both JS containers: **Notification toasts**: A new `LogBoxNotificationBackHandler` component mounts alongside the toasts and registers a `hardwareBackPress` listener that calls `clearWarnings()` + `clearErrors()`, equivalent to pressing X on every visible toast. The component returns null and auto-cleans the listener on unmount. **Inspector overlay**: `LogBoxInspectorContainer` registers a `hardwareBackPress` listener in `componentDidMount` that calls `_handleMinimize()` (`setSelectedLog(-1)`), closing the overlay non-destructively — same as pressing the Minimize button. Changelog: [Android][Added] - Allow LogBox notification toasts and inspector overlay to be dismissed via Android back button Reviewed By: alanleedev Differential Revision: D101178179 fbshipit-source-id: c2100d2cc494c326d8a91caa906faabb3d23381c
1 parent 370606d commit 6530092

14 files changed

Lines changed: 717 additions & 353 deletions

packages/react-native/Libraries/LogBox/LogBoxInspectorContainer.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type LogBoxLog from './Data/LogBoxLog';
1212

1313
import View from '../Components/View/View';
1414
import StyleSheet from '../StyleSheet/StyleSheet';
15+
import BackHandler from '../Utilities/BackHandler';
1516
import * as LogBoxData from './Data/LogBoxData';
1617
import LogBoxInspector from './UI/LogBoxInspector';
1718
import * as React from 'react';
@@ -23,6 +24,27 @@ type Props = Readonly<{
2324
}>;
2425

2526
export class _LogBoxInspectorContainer extends React.Component<Props> {
27+
_backHandler: ?{remove: () => void, ...} = null;
28+
29+
componentDidMount() {
30+
this._backHandler = BackHandler.addEventListener(
31+
'hardwareBackPress',
32+
() => {
33+
if (this.props.selectedLogIndex < 0) {
34+
return false;
35+
}
36+
this._handleMinimize();
37+
return true;
38+
},
39+
);
40+
}
41+
42+
componentWillUnmount() {
43+
if (this._backHandler) {
44+
this._backHandler.remove();
45+
}
46+
}
47+
2648
render(): React.Node {
2749
return (
2850
<View style={StyleSheet.absoluteFill}>

packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import SafeAreaView from '../../src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE';
1212
import View from '../Components/View/View';
1313
import StyleSheet from '../StyleSheet/StyleSheet';
14+
import BackHandler from '../Utilities/BackHandler';
1415
import * as LogBoxData from './Data/LogBoxData';
1516
import LogBoxLog from './Data/LogBoxLog';
1617
import LogBoxLogNotification from './UI/LogBoxNotification';
@@ -22,8 +23,28 @@ type Props = Readonly<{
2223
isDisabled?: ?boolean,
2324
}>;
2425

25-
export function _LogBoxNotificationContainer(props: Props): React.Node {
26+
function useLogBoxBackHandler(focused: boolean, logCount: number): void {
27+
React.useEffect(() => {
28+
if (!focused || logCount === 0) {
29+
return;
30+
}
31+
const subscription = BackHandler.addEventListener(
32+
'hardwareBackPress',
33+
() => {
34+
LogBoxData.clearWarnings();
35+
LogBoxData.clearErrors();
36+
return true;
37+
},
38+
);
39+
return () => subscription.remove();
40+
}, [focused, logCount]);
41+
}
42+
43+
export function LogBoxNotificationContainer(props: Props): React.Node {
2644
const {logs} = props;
45+
const [focused, setFocused] = React.useState(false);
46+
47+
useLogBoxBackHandler(focused, logs.length);
2748

2849
const onDismissWarns = () => {
2950
LogBoxData.clearWarnings();
@@ -68,6 +89,7 @@ export function _LogBoxNotificationContainer(props: Props): React.Node {
6889
totalLogCount={warnings.length}
6990
onPressOpen={() => openLog(warnings[warnings.length - 1])}
7091
onPressDismiss={onDismissWarns}
92+
onFocusChange={setFocused}
7193
/>
7294
</View>
7395
)}
@@ -79,6 +101,7 @@ export function _LogBoxNotificationContainer(props: Props): React.Node {
79101
totalLogCount={errors.length}
80102
onPressOpen={() => openLog(errors[errors.length - 1])}
81103
onPressDismiss={onDismissErrors}
104+
onFocusChange={setFocused}
82105
/>
83106
</View>
84107
)}
@@ -101,5 +124,5 @@ const styles = StyleSheet.create({
101124
});
102125

103126
export default LogBoxData.withSubscription(
104-
_LogBoxNotificationContainer,
127+
LogBoxNotificationContainer,
105128
) as React.ComponentType<{}>;

packages/react-native/Libraries/LogBox/UI/LogBoxButton.js

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
1212
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
1313
import type {GestureResponderEvent} from '../../Types/CoreEventTypes';
1414

15-
import TouchableWithoutFeedback from '../../Components/Touchable/TouchableWithoutFeedback';
15+
import Pressable from '../../Components/Pressable/Pressable';
1616
import View from '../../Components/View/View';
1717
import StyleSheet from '../../StyleSheet/StyleSheet';
1818
import * as LogBoxStyle from './LogBoxStyle';
@@ -28,9 +28,10 @@ component LogBoxButton(
2828
children?: React.Node,
2929
hitSlop?: ?EdgeInsetsProp,
3030
onPress?: ?(event: GestureResponderEvent) => void,
31+
onFocusChange?: ?(focused: boolean) => void,
3132
style?: ViewStyleProp,
3233
) {
33-
const [pressed, setPressed] = useState(false);
34+
const [focused, setFocused] = useState(false);
3435

3536
let resolvedBackgroundColor = backgroundColor;
3637
if (!resolvedBackgroundColor) {
@@ -40,32 +41,54 @@ component LogBoxButton(
4041
};
4142
}
4243

43-
const content = (
44-
<View
45-
id={id}
46-
style={StyleSheet.compose(
47-
{
48-
backgroundColor: pressed
49-
? resolvedBackgroundColor.pressed
50-
: resolvedBackgroundColor.default,
51-
},
52-
style,
53-
)}>
54-
{children}
55-
</View>
56-
);
44+
if (onPress == null) {
45+
return (
46+
<View
47+
id={id}
48+
style={StyleSheet.compose(
49+
{backgroundColor: resolvedBackgroundColor.default},
50+
style,
51+
)}>
52+
{children}
53+
</View>
54+
);
55+
}
5756

58-
return onPress == null ? (
59-
content
60-
) : (
61-
<TouchableWithoutFeedback
57+
return (
58+
<Pressable
59+
id={id}
60+
focusable={true}
6261
hitSlop={hitSlop}
6362
onPress={onPress}
64-
onPressIn={() => setPressed(true)}
65-
onPressOut={() => setPressed(false)}>
66-
{content}
67-
</TouchableWithoutFeedback>
63+
onFocus={() => {
64+
setFocused(true);
65+
onFocusChange?.(true);
66+
}}
67+
onBlur={() => {
68+
setFocused(false);
69+
onFocusChange?.(false);
70+
}}
71+
style={({pressed}) =>
72+
StyleSheet.compose(
73+
{
74+
backgroundColor: pressed
75+
? resolvedBackgroundColor.pressed
76+
: resolvedBackgroundColor.default,
77+
},
78+
focused ? StyleSheet.compose(style, styles.focusRing) : style,
79+
)
80+
}>
81+
{children}
82+
</Pressable>
6883
);
6984
}
7085

86+
const styles = StyleSheet.create({
87+
focusRing: {
88+
borderWidth: 2,
89+
borderColor: LogBoxStyle.getTextColor(0.6),
90+
borderRadius: 4,
91+
},
92+
});
93+
7194
export default LogBoxButton;

packages/react-native/Libraries/LogBox/UI/LogBoxInspector.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export default function LogBoxInspector(props: Props): React.Node {
5757
}, []);
5858

5959
useEffect(() => {
60+
if (log == null) {
61+
return;
62+
}
6063
const subscription = BackHandler.addEventListener(
6164
'hardwareBackPress',
6265
() => {
@@ -65,7 +68,7 @@ export default function LogBoxInspector(props: Props): React.Node {
6568
},
6669
);
6770
return () => subscription.remove();
68-
}, [onMinimize]);
71+
}, [log, onMinimize]);
6972

7073
function _handleRetry() {
7174
LogBoxData.retrySymbolicateLogNow(log);

packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import LogBoxButton from './LogBoxButton';
1919
import * as LogBoxStyle from './LogBoxStyle';
2020
import * as React from 'react';
2121

22+
const noop = () => {};
23+
2224
component LogBoxInspectorStackFrame(
2325
frame: StackFrame,
2426
onPress?: ?(event: GestureResponderEvent) => void,
@@ -38,7 +40,7 @@ component LogBoxInspectorStackFrame(
3840
default: 'transparent',
3941
pressed: onPress ? LogBoxStyle.getBackgroundColor(1) : 'transparent',
4042
}}
41-
onPress={onPress}
43+
onPress={onPress ?? noop}
4244
style={styles.frame}>
4345
<Text
4446
id="logbox_stack_frame_text"

packages/react-native/Libraries/LogBox/UI/LogBoxNotification.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Props = Readonly<{
2626
level: 'warn' | 'error',
2727
onPressOpen: () => void,
2828
onPressDismiss: () => void,
29+
onFocusChange?: ?(focused: boolean) => void,
2930
}>;
3031

3132
export default function LogBoxNotification(props: Props): React.Node {
@@ -41,6 +42,7 @@ export default function LogBoxNotification(props: Props): React.Node {
4142
<LogBoxButton
4243
id={`logbox_open_button_${level}`}
4344
onPress={props.onPressOpen}
45+
onFocusChange={props.onFocusChange}
4446
style={styles.press}
4547
backgroundColor={{
4648
default: LogBoxStyle.getBackgroundColor(1),

packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxButton-test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ const LogBoxButton = require('../LogBoxButton').default;
1414
const render = require('@react-native/jest-preset/jest/renderer');
1515
const React = require('react');
1616

17-
// Mock `TouchableWithoutFeedback` because we are interested in snapshotting the
18-
// behavior of `LogBoxButton`, not `TouchableWithoutFeedback`.
19-
jest.mock('../../../Components/Touchable/TouchableWithoutFeedback', () => ({
17+
// Mock `Pressable` because we are interested in snapshotting the
18+
// behavior of `LogBoxButton`, not `Pressable`.
19+
jest.mock('../../../Components/Pressable/Pressable', () => ({
2020
__esModule: true,
21-
default: 'TouchableWithoutFeedback',
21+
default: 'Pressable',
2222
}));
2323

2424
describe('LogBoxButton', () => {
@@ -36,7 +36,7 @@ describe('LogBoxButton', () => {
3636
expect(output).toMatchSnapshot();
3737
});
3838

39-
it('should render TouchableWithoutFeedback and pass through props', async () => {
39+
it('should render Pressable and pass through props', async () => {
4040
const output = await render.create(
4141
<LogBoxButton
4242
backgroundColor={{

packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxButton-test.js.snap

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`LogBoxButton should render TouchableWithoutFeedback and pass through props 1`] = `
4-
<TouchableWithoutFeedback
3+
exports[`LogBoxButton should render Pressable and pass through props 1`] = `
4+
<Pressable
5+
focusable={true}
56
hitSlop={Object {}}
7+
onBlur={[Function]}
8+
onFocus={[Function]}
69
onPress={[Function]}
7-
onPressIn={[Function]}
8-
onPressOut={[Function]}
10+
style={[Function]}
911
>
10-
<View
11-
style={
12-
Object {
13-
"backgroundColor": "black",
14-
}
15-
}
16-
>
17-
<Text>
18-
Press me
19-
</Text>
20-
</View>
21-
</TouchableWithoutFeedback>
12+
<Text>
13+
Press me
14+
</Text>
15+
</Pressable>
2216
`;
2317

2418
exports[`LogBoxButton should render only a view without an onPress 1`] = `

packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorStackFrame-test.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ exports[`LogBoxInspectorStackFrame should render stack frame without press feedb
156156
"pressed": "transparent",
157157
}
158158
}
159+
onPress={[Function]}
159160
style={
160161
Object {
161162
"borderRadius": 5,

packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorStackFrames-test.js.snap

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,35 @@ exports[`LogBoxInspectorStackFrames should render stack frames with 1 frame coll
7878
}
7979
>
8080
<View
81+
accessibilityState={
82+
Object {
83+
"busy": undefined,
84+
"checked": undefined,
85+
"disabled": undefined,
86+
"expanded": undefined,
87+
"selected": undefined,
88+
}
89+
}
90+
accessibilityValue={
91+
Object {
92+
"max": undefined,
93+
"min": undefined,
94+
"now": undefined,
95+
"text": undefined,
96+
}
97+
}
98+
accessible={true}
99+
collapsable={false}
100+
focusable={true}
101+
onBlur={[Function]}
102+
onClick={[Function]}
103+
onFocus={[Function]}
104+
onResponderGrant={[Function]}
105+
onResponderMove={[Function]}
106+
onResponderRelease={[Function]}
107+
onResponderTerminate={[Function]}
108+
onResponderTerminationRequest={[Function]}
109+
onStartShouldSetResponder={[Function]}
81110
style={
82111
Array [
83112
Object {
@@ -149,9 +178,20 @@ exports[`LogBoxInspectorStackFrames should render stack frames with 1 frame coll
149178
"selected": undefined,
150179
}
151180
}
181+
accessibilityValue={
182+
Object {
183+
"max": undefined,
184+
"min": undefined,
185+
"now": undefined,
186+
"text": undefined,
187+
}
188+
}
152189
accessible={true}
190+
collapsable={false}
153191
focusable={true}
192+
onBlur={[Function]}
154193
onClick={[Function]}
194+
onFocus={[Function]}
155195
onResponderGrant={[Function]}
156196
onResponderMove={[Function]}
157197
onResponderRelease={[Function]}

0 commit comments

Comments
 (0)