Skip to content
Draft
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
92 changes: 87 additions & 5 deletions demo/src/screens/componentScreens/ActionSheetScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import {View, Text, Button, ActionSheet} from 'react-native-ui-lib'; //eslint-disable-line
import {Alert, Modal, Platform, View as RNView} from 'react-native';
import {View, Text, Button, ActionSheet, Dialog} from 'react-native-ui-lib'; //eslint-disable-line
import _ from 'lodash';

const useCases = [
Expand All @@ -11,12 +12,24 @@ const collectionsIcon = require('../../assets/icons/collections.png');
const starIcon = require('../../assets/icons/star.png');
const shareIcon = require('../../assets/icons/share.png');

interface State {
showNative: boolean;
showCustom: boolean;
showCustomIcons: boolean;
showModal: boolean;
modalKey: number;
showDialog: boolean;
pickedOption?: string;
}

export default class ActionSheetScreen extends Component {
state = {
export default class ActionSheetScreen extends Component<{}, State> {
state: State = {
showNative: false,
showCustom: false,
showCustomIcons: false,
showModal: false,
modalKey: 0,
showDialog: false,
pickedOption: undefined
};

Expand All @@ -26,8 +39,19 @@ export default class ActionSheetScreen extends Component {
});
}

showReactNativeModal = () => {
this.setState(prevState => ({
showModal: true,
modalKey: prevState.modalKey + 1
}));
};

dismissReactNativeModal = () => {
this.setState({showModal: false});
};

render() {
const {showCustom, showCustomIcons, showNative, pickedOption} = this.state;
const {showCustom, showCustomIcons, showNative, showModal, modalKey, showDialog, pickedOption} = this.state;
return (
<View flex padding-25>
<Text text30>Action Sheet</Text>
Expand All @@ -46,7 +70,7 @@ export default class ActionSheetScreen extends Component {
this.setState({
showNative: useCase.useNativeIOS,
showCustom: !useCase.useNativeIOS && !useCase.icons,
showCustomIcons: !useCase.useNativeIOS && useCase.icons
showCustomIcons: !useCase.useNativeIOS && !!useCase.icons
})}
/>
);
Expand All @@ -58,6 +82,64 @@ export default class ActionSheetScreen extends Component {
</View>
)}

<View flex bottom>
<Button
marginB-10
label="Show Action Sheet"
onPress={() => this.setState({showCustom: true})}
/>
<Button
marginB-10
label="Show React Native Dialog"
onPress={() => this.setState({showDialog: true})}
/>
<Button
marginB-10
label="Show React Native Modal"
onPress={this.showReactNativeModal}
/>
<Button
label="Show React Native Alert"
onPress={() =>
Alert.alert('React Native Alert', 'This is a native Alert from React Native', [{text: 'OK'}])}
/>
</View>

<Dialog
visible={showDialog}
ignoreBackgroundPress
modalProps={{
onBackgroundPress: () => this.setState({showDialog: false}),
onRequestClose: () => this.setState({showDialog: false}),
onDismiss: () => this.setState({showDialog: false})
}}
onDismiss={() => this.setState({showDialog: false})}
>
<View padding-25 bg-white br40>
<Text text60 marginB-10>React Native UI Lib Dialog</Text>
<Text marginB-20>This is a Dialog from react-native-ui-lib</Text>
<Button label="Close" onPress={() => this.setState({showDialog: false})}/>
</View>
</Dialog>
{showModal && (
<RNView key={`rn-modal-${modalKey}`}>
<Modal
visible
animationType={Platform.OS === 'android' ? 'none' : 'slide'}
hardwareAccelerated
transparent
onRequestClose={this.dismissReactNativeModal}
>
<View flex center backgroundColor="rgba(0,0,0,0.5)">
<View padding-25 bg-white br40 center>
<Text text60 marginB-10>React Native Modal</Text>
<Text marginB-20>This is a native Modal from React Native</Text>
<Button label="Close" onPress={this.dismissReactNativeModal}/>
</View>
</View>
</Modal>
</RNView>
)}
<ActionSheet
title={'Title'}
message={'Message of action sheet'}
Expand Down
54 changes: 54 additions & 0 deletions summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# React Native Modal Android Back Button Investigation

## Issue

In `demo/src/screens/componentScreens/ActionSheetScreen.tsx`, closing a React Native `Modal` with the Android hardware back button left the underlying screen in a bad state.

Observed behavior:

- After the first hardware-back dismissal, buttons underneath the modal were not fully tappable.
- Later attempts showed the button press feedback, but the modal did not open again.
- The issue reproduced on a physical Android device.

## Relevant Findings

- React Native documents `Modal.onRequestClose` as the Android back-button callback. While a `Modal` is open, normal `BackHandler` events are not emitted.
- React Native documents `Modal.onDismiss` as iOS-only. Android `onDismiss` support is still not reliable; there is an open React Native proposal around adding Android support.
- Known Android reports around RN `Modal` mention touch/press events becoming blocked after modal usage, especially in RN 0.71+ and newer.
- A common workaround reported online is wrapping the `Modal` in a plain React Native `View`.
- Disabling dismissal animation can help in some `react-native-navigation` cases, but it did not solve this issue by itself.

## Tried and Failed

- Fixed JSX indentation only.
- This solved lint formatting, but did not address the Android modal issue.
- Controlled the modal with `visible={showModal}` instead of conditionally mounting it with a hard-coded `visible`.
- The issue still reproduced.
- Switched from React Native's raw `Modal` to `react-native-ui-lib`'s `Modal` wrapper.
- The issue still reproduced on device.
- Removed reliance on Android `onDismiss`.
- The issue still reproduced.
- Disabled Android modal animation with `animationType="none"`.
- The issue still reproduced.
- Added a delayed two-phase unmount after setting `visible={false}`.
- This caused a new problem: button press feedback appeared, but the modal did not open again after the first hardware-back close.

## Workaround That Worked

The working workaround keeps React Native's raw `Modal`, but:

- Conditionally mounts the modal only while `showModal` is true.
- Wraps the modal in a plain React Native `View` (`RNView`), not a UI-lib `View`.
- Forces a fresh native modal instance on each open by incrementing `modalKey` and using it in the wrapper key.
- Handles Android hardware back only through `onRequestClose`.
- Keeps Android `animationType="none"`.
- Enables `hardwareAccelerated`.

The user verified this workaround on an Android device.

## Current Implementation Notes

- `showModal` controls whether the modal is rendered.
- `modalKey` is incremented before each open so Android does not reuse the previously dismissed native modal instance.
- `dismissReactNativeModal` only sets `showModal` to `false`.
- The workaround is currently scoped to the demo screen and does not change the shared `Modal` or `Dialog` components.
Loading