* Copyright (c) 2025 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {TestSuite} from '@rnoh/testerino';
import React, {useState} from 'react';
import {
Modal,
StyleSheet,
Text,
View,
ModalProps,
TextInput,
FlatList,
Pressable,
} from 'react-native';
import {Button, TestCase} from '../components';
import {PALETTE} from '../components/palette';
export function ModalTest() {
return (
<TestSuite name="Modal">
<TestCase.Example itShould="show modal">
<ModalExample />
</TestCase.Example>
<TestCase.Manual
itShould="trigger onShow event after modal is shown"
initialState={false}
arrange={({setState}) => (
<ModalExample
onShow={() => {
setState(true);
}}
/>
)}
assert={({state, expect}) => {
expect(state).to.be.true;
}}
/>
<TestCase.Manual
itShould="trigger onDismiss event after modal is closed"
initialState={false}
arrange={({setState}) => (
<ModalExample
onDismiss={() => {
setState(true);
}}
/>
)}
assert={({state, expect}) => {
expect(state).to.be.true;
}}
/>
<TestCase.Example itShould="show orientation change history when Modal is open">
<ModalExampleOrientation />
</TestCase.Example>
<TestCase.Manual
itShould="show modal with slide animation"
initialState={false}
arrange={({setState}) => (
<ModalExample
animationType="slide"
onShow={() => {
setState(true);
}}
/>
)}
assert={({state, expect}) => {
expect(state).to.be.true;
}}
/>
<TestCase.Manual
itShould="show modal with fade animation"
initialState={false}
arrange={({setState}) => (
<ModalExample
animationType="fade"
onShow={() => {
setState(true);
}}
/>
)}
assert={({state, expect}) => {
expect(state).to.be.true;
}}
/>
<TestCase.Example itShould="soft keyboard should cover modal">
<ModalExample withTextInput hideTitle />
</TestCase.Example>
<TestCase.Example itShould="allow to display one modal on top of another">
<NestedModalinModal />
</TestCase.Example>
<TestCase.Example itShould="display modal from flatlist item">
<NestedModalInFlatlist />
</TestCase.Example>
<TestCase.Example itShould="long-pressing to close the Modal will not block the next touch action.">
<CloseModalByLongPress />
</TestCase.Example>
</TestSuite>
);
}
const ModalExampleOrientation = () => {
const [orientationHistory, setOrientationHistory] = useState<string[]>([]);
const [modalVisible, setModalVisible] = useState(false);
return (
<View>
<Text style={[styles.textStyle, {color: 'black'}]}>
{modalVisible ? 'shown' : 'hidden'}
</Text>
<Modal
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}
onOrientationChange={event => {
const orientation = event.nativeEvent.orientation;
setOrientationHistory(prev => [...prev, `${orientation}`]);
}}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={{marginVertical: 20}}>
Orientation history: {JSON.stringify(orientationHistory)}
</Text>
<Button
label="Hide Modal"
onPress={() => {
setModalVisible(false);
setOrientationHistory([]);
}}
/>
</View>
</View>
</Modal>
<Button label="Show Modal" onPress={() => setModalVisible(true)} />
</View>
);
};
const ModalExample = (
props: ModalProps & {withTextInput?: boolean; hideTitle?: boolean},
) => {
const [modalVisible, setModalVisible] = useState(false);
return (
<View>
<Text style={[styles.textStyle, {color: 'black'}]}>
{modalVisible ? 'shown' : 'hidden'}
</Text>
<Modal
{...props}
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
{props.hideTitle ? null : (
<Text style={styles.modalText}>Hello World!</Text>
)}
<Button label="Hide Modal" onPress={() => setModalVisible(false)} />
{props.withTextInput && <TextInput style={styles.textInput} />}
</View>
</View>
</Modal>
<Button label="Show Modal" onPress={() => setModalVisible(true)} />
</View>
);
};
const NestedModalinModal = () => {
const [firstModalVisible, setFirstModalVisible] = useState(false);
const [secondModalVisible, setSecondModalVisible] = useState(false);
return (
<View>
<Text style={[styles.textStyle, {color: 'black'}]}>
First: {firstModalVisible ? 'shown' : 'hidden'}
</Text>
<Text style={[styles.textStyle, {color: 'black'}]}>
Second: {secondModalVisible ? 'shown' : 'hidden'}
</Text>
<Modal
animationType="fade"
transparent={true}
visible={firstModalVisible}
onRequestClose={() => {
setFirstModalVisible(!firstModalVisible);
}}>
<View style={styles.centeredView}>
<View style={[styles.modalView, {padding: 60}]}>
<Text style={styles.modalText}>Hello World from first Modal!</Text>
<Modal
animationType="slide"
transparent={true}
visible={secondModalVisible}
onRequestClose={() => {
setSecondModalVisible(!secondModalVisible);
}}>
<View style={styles.centeredView}>
<View
style={[styles.modalView, {backgroundColor: 'lightblue'}]}>
<Text style={styles.modalText}>
Hello World from second Modal!
</Text>
<Button
label="Hide Second Modal"
onPress={() => setSecondModalVisible(false)}
/>
</View>
</View>
</Modal>
<Button
label="Hide Modal"
onPress={() => setFirstModalVisible(false)}
/>
<Button
label="Open another Modal"
onPress={() => setSecondModalVisible(true)}
/>
</View>
</View>
</Modal>
<Button label="Show Modal" onPress={() => setFirstModalVisible(true)} />
</View>
);
};
const DATA = [
{
id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
title: 'First Item',
},
{
id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
title: 'Second Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d72',
title: 'Third Item',
},
];
type ItemProps = {title: string};
const Item = ({title}: ItemProps) => {
const [modalVisible, setModalVisible] = useState(false);
return (
<View>
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={styles.modalText}>{title}</Text>
<Button label="Hide Modal" onPress={() => setModalVisible(false)} />
</View>
</View>
</Modal>
<View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<Text>{title}</Text>
<Button label="Show Modal" onPress={() => setModalVisible(true)} />
</View>
</View>
);
};
const NestedModalInFlatlist = () => {
return (
<>
<FlatList
data={DATA}
renderItem={({item}) => <Item title={item.title} />}
keyExtractor={item => item.id}
/>
</>
);
};
const CloseModalByLongPress = () => {
const [modalVisible, setModalVisible] = useState(false);
return (
<>
<Modal
visible={modalVisible}
animationType="slide"
transparent={true}
onRequestClose={() => {
setModalVisible(false);
}}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={styles.modalText}>
After the Modal is closed, click "Show Modal" again.
</Text>
<Pressable
style={({pressed}) => [
styles.longPressButton,
{
backgroundColor: pressed
? PALETTE.REACT_CYAN_DARK
: PALETTE.REACT_CYAN_LIGHT,
},
]}
onLongPress={() => setModalVisible(false)}>
<Text>Long-pressing to close</Text>
</Pressable>
</View>
</View>
</Modal>
<Button label="Show Modal" onPress={() => setModalVisible(true)} />
</>
);
};
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginTop: 22,
},
modalView: {
margin: 20,
backgroundColor: 'white',
borderRadius: 20,
padding: 35,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
button: {
borderRadius: 20,
padding: 10,
elevation: 2,
},
buttonOpen: {
backgroundColor: '#F194FF',
},
buttonClose: {
backgroundColor: '#2196F3',
},
textStyle: {
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
height: 20,
},
modalText: {
marginBottom: 15,
textAlign: 'center',
height: 20,
},
textInput: {
width: 150,
height: 40,
backgroundColor: 'rgb(200,200,200)',
borderRadius: 12,
marginVertical: 4,
},
longPressButton: {
paddingVertical: 6,
paddingHorizontal: 12,
borderWidth: 2,
borderColor: PALETTE.REACT_CYAN_DARK,
},
});