How to make native-base Modal works well with KeyboardAvoildingView in Android? - react-native

I try to add KeyboardAvoidingView to the Modal Component.
But when i call the keyboard up, the modal doesnt move and still be covered by keyboard.
This is my code : https://snack.expo.dev/#tikkhun/joyous-blueberries

After searching and asking. I get a way to work well: just use behavior: "position"
Here is My example Component:
/**
* #file: 弹出框
*/
import React, { useRef, useEffect, useState } from 'react';
import { Center, Button, HStack, Input, KeyboardAvoidingView, Modal, Spacer, Text } from 'native-base';
import { useTranslation } from 'react-i18next';
export default function ModalContent({ isOpen, onClose, title, defaultValue, type = 'input', onSave }) {
const { t } = useTranslation();
const [value, setValue] = useState();
const inputRef = useRef(null);
useEffect(() => {
// 这里的 setTimeout 是为了让键盘正常弹出
setTimeout(() => {
if (inputRef?.current) {
inputRef.current.focus();
}
}, 10);
});
useEffect(() => {
setValue(defaultValue);
return () => {
setValue('');
};
});
return (
<Modal isOpen={isOpen} onClose={onClose}>
<KeyboardAvoidingView style={{ width: '100%' }} behavior="position">
<Center>
<Modal.Content style={{ width: '100%' }}>
<Modal.Header>
<HStack space="3" alignItems="center">
<Text fontSize="md">{title}</Text>
<Spacer />
<Button
_text={{ fontSize: 'md' }}
variant="ghost"
onPress={() => {
onSave && onSave(value);
}}>
{t('settings.save')}
</Button>
</HStack>
</Modal.Header>
<Modal.Body>
<Input size="2xl" ref={inputRef} defaultValue={value} onChangeText={v => setValue(v)} />
</Modal.Body>
</Modal.Content>
</Center>
</KeyboardAvoidingView>
</Modal>
);
}

Related

How does onBlur event work in react native? it doesn't work i want it to

I made an expo app.
I want to make an AutoCompleteInput.
When focusing and changing TextInput, It will render the same words.
When ListItem clicks, TextInput's value will update it and it doesn't render.
When TextInput focus out, ListItem doesn't render.
In my code, the onPress event works first. onPress event works after.
how can i do?
and how does work onBlur event?
i moved onBlur event other component, but it doesn't work...
// App.js
...
return (
<View style={styles.container}>
<View style={styles.contentContainer}>
<Text>my content</Text>
</View>
<AutoInput />
<StatusBar style="auto" />
</View>
);
// AutoInput.jsx
import { ListItem, TextInput } from '#react-native-material/core';
import { useCallback, useState } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { locations } from '../../utils/constants';
const initialLocation = locations.map(({ name }) => name);
const AutoInput = () => {
const [location, setLocation] = useState('');
const [searchedLocations, setSearchedLocations] = useState([]);
const handleChangeInputText = useCallback(
(newString) => {
setLocation(newString);
if (!newString) {
setSearchedLocations(initialLocation);
return;
}
const filteredLocations = locations
.filter(({ name }) => name.includes(newString))
.map(({ name }) => name);
setSearchedLocations(filteredLocations);
},
[initialLocation]
);
return (
<View style={styles.container}>
<TextInput
placeholder="지역을 검색해주세요"
onBlur={() => {
console.log('blur');
setSearchedLocations([]);
}}
onFocus={() => console.log('focus')}
onChangeText={handleChangeInputText}
value={location}
/>
<ScrollView style={styles.scroll}>
{searchedLocations.map((searchLocation) => (
<ListItem
key={searchLocation}
title={searchLocation}
onPress={() => alert(searchLocation)}
style={styles.listItem}
/>
))}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: { position: 'absolute', width: '100%' },
scroll: { maxHeight: 300 },
listItem: { width: '100%' },
});
export default AutoInput;
enter image description here

TouchableOpacity does not show disabled and activeOpacity props

I am trying to test this simple button:
import React, { FC, ReactNode } from 'react';
import { TouchableOpacity, GestureResponderEvent, ViewStyle } from 'react-native';
type Props = {
style: ViewStyle;
onPress: (event: GestureResponderEvent) => void;
disabled?: boolean;
activeOpacity?: number;
children: ReactNode;
};
export const Button: FC<Props> = ({
style,
onPress,
disabled,
activeOpacity,
children
}) => {
return (
<TouchableOpacity
activeOpacity={ activeOpacity }
onPress={ onPress }
style={ style }
disabled={ disabled }
testID={ 'button' }
>
{ children }
</TouchableOpacity>
);
};
I use this test file, where I simply render my Button with some props:
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { render } from '#testing-library/react-native';
import { ReactTestInstance } from 'react-test-renderer';
import { Button } from '../../../src/components/Button';
const styles = StyleSheet.create({
button: {
height: 50
}
});
const onPressMock = jest.fn();
describe('FilterForm', () => {
it('should render Button with default arguments', () => {
const { queryByText, debug } = render(
<Button style={ styles.button } onPress={ onPressMock } disabled activeOpacity={ 0.3 }>
<Text>{ 'Dummy Test Text' }</Text>
</Button>
);
debug();
// Not important - just in case you are curious //
let buttonText = queryByText('Dummy Test Text');
expect(buttonText).not.toBeNull();
buttonText = buttonText as ReactTestInstance;
expect(buttonText.parent?.parent?.props.testID).toEqual('button');
expect(buttonText.parent?.parent?.props.activeOpacity).toEqual(0.3);
expect(buttonText.parent?.parent?.props.disabled).toEqual(true);
});
});
The problem is that I get this tree returned, which does not have disabled or activeOpacity in it:
<View
accessible={true}
focusable={true}
onClick={[Function onClick]}
onResponderGrant={[Function onResponderGrant]}
onResponderMove={[Function onResponderMove]}
onResponderRelease={[Function onResponderRelease]}
onResponderTerminate={[Function onResponderTerminate]}
onResponderTerminationRequest={[Function onResponderTerminationRequest]}
onStartShouldSetResponder={[Function onStartShouldSetResponder]}
style={
Object {
"height": 50,
"opacity": 1,
}
}
testID="button"
>
<Text>
Dummy Test Text
</Text>
</View>
Because of that my assertions in the test file above fail. How can I test the props of TouchableOpacity then?
Thanks in advance for your time!
I can call disabled prop by using fireEvent(button, 'press'). Disabled button will not call the handler, so I can assert it with expect(handlerMock).not.toBeCalled().
As to activeOpacity, I guess storybook should be used for visual testing.

Error when simulate change text react native with jest

Let's say I create a login screen. Inside that screen, I import form component. And inside form component I have a text input.
Then, I want to simulate text input on change text, but always get an error
Method “simulate” is meant to be run on 1 node. 0 found instead.
This is my test file
it('calls the login submit method', () => {
const fieldPhoneNumber = wrapper
.find('Form')
.dive()
.find('TextInput[id="fieldPhoneNumber"]');
fieldPhoneNumber
.at(0)
.simulate('changeText', { target: { value: '082262366193' } });
});
This is my component login file
import React, { useState, useEffect } from 'react';
import { ScrollView, StatusBar, Platform } from 'react-native';
import Header from './components/Header';
import Form from './components/Form';
import ButtonSocialMedia from './components/ButtonSocialMedia';
function LoginScreen() {
const [phoneNumber, setPhoneNumber] = useState('');
const [focus, setFocus] = useState(false);
useEffect(() => {
}, [phoneNumber]);
const changePhoneNumber = (value) => {
setPhoneNumber(value);
};
const showAppleButton = () => {
if (Platform.OS === 'ios') {
const version = Platform.Version.split('.')[0];
if (version >= 13) {
return true;
} else {
return false;
}
} else {
return false;
}
};
const loginSubmit = () => {
console.log('Login Submit');
};
return (
<ScrollView>
<StatusBar
translucent
backgroundColor="transparent"
barStyle="light-content"
/>
<Header />
<Form
phoneNumber={phoneNumber}
changePhoneNumber={(value) => changePhoneNumber(value)}
focus={focus}
setFocus={() => setFocus(true)}
loginSubmit={() => loginSubmit()} />
<ButtonSocialMedia showAppleButton={() => showAppleButton()} />
</ScrollView>
);
}
export default LoginScreen;
This is my form component
/* eslint-disable prettier/prettier */
import React from 'react';
import { View, Text, TextInput } from 'react-native';
import styles from '../styles/StyleForm';
import color from '../../../../__global__/styles/themes/colorThemes';
import regex from '../../../../constant/regex';
import * as yup from 'yup';
import { Formik } from 'formik';
import ButtonFull from '../../../../__global__/button/buttonFull';
const regexPhoneNumber = regex.phone;
function Form(props) {
const renderFocus = () => {
if (props.focus) {
return (
<Text style={styles.textFocus}>Type your phone number</Text>
);
}
};
return (
<Formik
enableReinitialize={true}
initialValues={{
phoneNumber: props.phoneNumber,
}}
onSubmit={values => {
console.log('Login Submit');
}}
validateOnMount={true}
validationSchema={yup.object().shape({
phoneNumber: yup
.string()
.required()
.min(8)
.matches(regexPhoneNumber, 'Phone number is not valid'),
})}>
{({
// values,
handleChange,
errors,
setFieldTouched,
touched,
isValid,
handleSubmit,
}) => (
<View style={styles.form}>
<View style={styles.subContainer}>
<View style={styles.containerTitle}>
<Text style={styles.textTitle}>+62</Text>
</View>
<View style={styles.containerPhoneNumber}>
{renderFocus()}
<TextInput
id={'fieldPhoneNumber'}
onFocus={() => props.setFocus(true)}
value={props.phoneNumber}
style={styles.subContainerPhoneNumber}
placeholderStyle={styles.placeholder}
placeholder={'Type your phone number'}
onChangeText={(value) => {
handleChange('phoneNumber');
props.changePhoneNumber(value);
setFieldTouched('phoneNumber', true);
}}
keyboardType={'numeric'}
onBlur={() => setFieldTouched('phoneNumber', true)}
/>
</View>
</View>
{touched.phoneNumber && errors.phoneNumber && (
<View style={styles.containerError}>
<Text style={styles.textError}>Phone number is not valid</Text>
</View>
)}
<View style={styles.containerButton}>
<ButtonFull
isDisabled={!isValid}
id={'buttonLogin'}
color={isValid ? color.thema : color.grey}
handleSubmit={() => props.loginSubmit()}
title={'Next'}
/>
</View>
</View>
)}
</Formik>
);
}
export default Form;
The error you're facing implies that the statement const fieldPhoneNumber wrapper.find('Form').dive().find('TextInput[id="fieldPhoneNumber"]'); couldn't find the TextInput component and hence the simulate function cannot be called. Try searching for the string "TextInput" inside the wrapper, and see if that works.

How do I find the component position in a functional react-native component

I am trying to dynamically find the position of my component after it has rendered. Im trying to use useRef and getBoundingClientRect() (see code below).
The response I get is this.
myRef.current.getBoundingClientRect is not a function. (In 'myRef.current.getBoundingClientRect()', 'myRef.current.getBoundingClientRect' is undefined).
Any ideas what I am doing wrong?
import React, { useState, useRef } from "react";
import { View, Button, TextInput } from "react-native";
const MyComponent = () => {
const [value, onChangeText] = useState("Useless Placeholder");
const myRef = useRef();
const showRefPosition = () => {
console.log("button clicked, set focus and log position");
// this works and shows that i am using the ref correctly
myRef.current.focus();
// however, this does not work and throws the eror
let componentPosition = myRef.current.getBoundingClientRect();
console.log(`Component Position ${componentPosition}`);
};
return (
<View>
<TextInput
style={{ height: 40, borderColor: "gray", borderWidth: 1 }}
onChangeText={text => onChangeText(text)}
value={value}
ref={myRef}
/>
<Button title="Click Me" onPress={() => showRefPosition()} />
</View>
);
};
export default MyComponent;
You can try measure method in react-native.
import React, { useState, useRef } from "react";
import { View, Button, TextInput } from "react-native";
const MyComponent = () => {
const [value, onChangeText] = useState("Useless Placeholder");
const myRef = useRef();
const showRefPosition = () => {
console.log("button clicked, set focus and log position");
// this works and shows that i am using the ref correctly
this.ref.measure( (width, height) => {
console.log('Component width is: ' + width)
console.log('Component height is: ' + height)
})
};
return (
<View>
<TextInput
style={{ height: 40, borderColor: "gray", borderWidth: 1 }}
onChangeText={text => onChangeText(text)}
value={value}
ref={(ref) => { this.ref = ref; }}
/>
<Button title="Click Me" onPress={() => showRefPosition()} />
</View>
);
};
export default MyComponent;

react native stack navigation undefined is not an object (evalutating 'props.navigation')

‌‌I'm making an app in react-native and I would like to navigate to different page by clicking on a button using stack navigation:
Here is my code :
app.js
import React from 'react';
import { AppRegistry } from 'react-native';
import { StackNavigator } from 'react-navigation';
import Home from './Screens/Home';
import VideoListItems from './Screens/VideoListItems';
import TrackPlayer from './Screens/TrackPlayer';
const reactNavigationSample = props => {
return <VideoListItems navigation={props.navigation} />;
};
reactNavigationSample.navigationOptions = {
title: "VideoListItems"
};
const AppNavigator = StackNavigator({
Home: { screen: Home, navigationOptions: { header: null }},
VideoListItems: { screen: VideoListItems, navigationOptions: { header: null }},
TrackPlayer: { screen: TrackPlayer, navigationOptions: { header: null }},
}
);
export default class App extends React.Component {
render() {
return (
<AppNavigator />
);
}
}
VideoListItems where the button to navigate is :
import {StackNavigator} from 'react-navigation';
const VideoListItems = ({ video, props }) => {
const { navigate } = props.navigation;
const {
cardStyle,
imageStyle,
contentStyle,
titleStyle,
channelTitleStyle,
descriptionStyle
} = styles;
const {
title,
channelTitle,
description,
thumbnails: { medium: { url } }
} = video.snippet;
const videoId = video.id.videoId;
return(
<View>
<Card title={null} containerStyle={cardStyle}>
<Image
style={imageStyle}
source= {{ uri: url}}
/>
<View style={contentStyle}>
<Text style={titleStyle}>
{title}
</Text>
<Text style={channelTitleStyle}>
{channelTitle}
</Text>
<Text style={descriptionStyle}>
{description}
</Text>
<Button
raised
title="Save And Play"
icon={{ name: 'play-arrow' }}
containerViewStyle={{ marginTop: 10 }}
backgroundColor="#E62117"
onPress={() => {
navigate('TrackPlayer')
}}
/>
</View>
</Card>
</View>
);
};
export default VideoListItems;
But I'm getting this error :
TypeError: undefined is not an object (evaluating 'props.navigation')
I don't know how to pass the props navigation and make it able to navigate when clicking on the button, I don't know where is my error, any ideas ?
[EDIT]
My new VideoItemList :
const VideoListItems = props => {
const {
cardStyle,
imageStyle,
contentStyle,
titleStyle,
channelTitleStyle,
descriptionStyle
} = styles;
const {
title,
channelTitle,
description,
thumbnails: { medium: { url } }
} = props.video.snippet;
const videoId = props.video.id.videoId;
const { navigate } = props.navigation;
return(
<View>
<Card title={null} containerStyle={cardStyle}>
<Image
style={imageStyle}
source= {{ uri: url}}
/>
<View style={contentStyle}>
<Text style={titleStyle}>
{title}
</Text>
<Text style={channelTitleStyle}>
{channelTitle}
</Text>
<Text style={descriptionStyle}>
{description}
</Text>
<Button
raised
title="Save And Play"
icon={{ name: 'play-arrow' }}
containerViewStyle={{ marginTop: 10 }}
backgroundColor="#E62117"
onPress={() => {
navigate.navigate('TrackPlayer')
}}
/>
</View>
</Card>
</View>
);
};
This is the file where I display all my components :
import React, { Component } from 'react';
import { View } from 'react-native';
import YTSearch from 'youtube-api-search';
import AppHeader from './AppHeader';
import SearchBar from './SearchBar';
import VideoList from './VideoList';
const API_KEY = 'ApiKey';
export default class Home extends Component {
state = {
loading: false,
videos: []
}
componentWillMount(){
this.searchYT('');
}
onPressSearch = term => {
this.searchYT(term);
}
searchYT = term => {
this.setState({ loading: true });
YTSearch({key: API_KEY, term }, videos => {
console.log(videos);
this.setState({
loading: false,
videos
});
});
}
render() {
const { loading, videos } = this.state;
return (
<View style={{ flex: 1, backgroundColor: '#ddd' }}>
<AppHeader headerText="Project Master Sound Control" />
<SearchBar
loading={loading}
onPressSearch={this.onPressSearch} />
<VideoList videos={videos} />
</View>
);
}
}
And my VideoList where I use VideoListItems :
import React from 'react';
import { ScrollView, View } from 'react-native';
import VideoListItems from './VideoListItems';
const VideoList = ({ videos }) => {
const videoItems = videos.map(video =>(
<VideoListItems
key={video.etag}
video={video}
/>
));
return(
<ScrollView>
<View style={styles.containerStyle}>
{videoItems}
</View>
</ScrollView>
);
};
const styles = {
containerStyle: {
marginBottom: 10,
marginLeft: 10,
marginRight: 10
}
}
export default VideoList;
That's because you try to extract navigation from a prop named props (that doesn't exist), you have many ways to solve this problem :
Use the rest operator to group all props except video inside a props variable
const VideoListItems = ({ video, ...props }) => {
Don't destructure you props object
const VideoListItems = props => {
// don't forget to refactor this line
const videoId = props.video.id.videoId;
Extract navigation from props
const VideoListItems = ({ video, navigation }) => {
const { navigate } = navigation;