React Native Testing Library: scroll FlatList to bottom - react-native

I have a react-native FlatList component with onEndReached event used to load more items.
I am testing the component with react-native-testing-library. The test is as follows:
Component mounted.
Requests 10 items to mockedBackend.
Expect FlatList to render those 10 items.
fireEvent onEndReached to ask for next page with some more items.
Expect FlatList to render new items.
Test was failing because FlatList was not rendering new items even thought FlatList's data property was being updated (I used act and waitFor as needed).
I managed to make the test pass when I fired onScroll event to bottom after some tries with hardcoded values.
Is there a way to make a helper to scroll to bottom taking into account proper FlatList size?
Helper to improve:
export const scrollListToBottom = (list: ReactTestInstance) => {
// FIXME: improve to get real list size
// After some tries these hardcoded values allowed to pass the test
act(() => {
fireEvent.scroll(list, {
nativeEvent: {
contentSize: {height: 500, width: 100},
contentOffset: {y: 400, x: 0},
layoutMeasurement: {height: 100, width: 100},
}
})
})
// FIXME: if onScroll works perfectly is this even needed?
act(() => {
fireEvent(list, 'onEndReached')
})
}
Thank you.

Related

How to make a React Native TextInput change Opacity just like TouchableOpacity?

My code looks something like this currently:
<View>
<TextInput placeholder='PlaceholderText'>
</TextInput>
</View>
I want to make a TextInput component that has an opacity animation on click (exactly like TouchableOpacity changes opacity on click).
I tried wrapping the TextInput inside TouchableOpacity, but it doesn't work since the touchable component surrounds text input. Is there a standard React Native or StyleSheet way of doing this or would I have to manually create an animation to mimic that effect?
Simply wrap your TextInput in a View element and animate the View's opacity color from the onFocs event of the TextInput. TextInput doesn't have the opacity attribute, therefore the additional view is required for your goal.
Following code may give you an idea how to solve it. Have to admit, that I haven't tested the code, it may contain some error, so use it carefully.
// First create an animated value to use for the view's opacity.
const textInputAnimOpacity = useRef(new Animated.Value(1.0)).current;
// create a method to set the opacity back to 1.0
const showTxtInput = () => {
Animated.timing(textInputAnimOpacity, {
toValue: 1.0, // value to reach
duration: 250 // time in ms
}).start();
};
// this animated method differs from the first one by (obviously the value 0.7)
//and the callback part that goes into the start()-method
const blurTxtInput = () => {
Animated.timing(textInputAnimOpacity, {
toValue: 0.7, // value to reach
duration: 250 // time in ms
}).start(({ finished }) => {
showTxtInput(); // Callback after finish, setting opacity back to 1.0
});
};
/*Set the opacity to the value, we want to animate*/
<View style={{opacity:textInputAnimOpacity}}>
/* call blurTxtInput to set the value to 0.7 and again to 1.0 once it reaches 0.7*/
<TextInput onPressIn={blurTxtInput} placeholder='PlaceholderText'>
</TextInput>
</View>
If you just want to set opacity, make your styles change using the onPressIn and onPressOut props:
const [pressed, setPressed] = useState(false);
// in render
<TextInput
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
style={pressed ? styles.textInputPressed : styles.textInput}
// ...
/>
If you need the changes to animate, you can do that with the built-in RN Animated component or react-native-reanimated, using the same props to trigger the animations.

Implementing a paginated slider in react native

Looking for ideas on how to implement a Netflix.com like slider in react native.
If I use a ScrollView, user can scroll all the way to right with one single momentum scroll. But Netflix slider scrolls only 6 (itemsPerRow) items at a time. So, ScrollView may not be ideal. I will use general View instead of ScrollView.
<View style={styles.slider}>
<SliderControl pressHandler={loadPrevItems}>
<Text>{'<'}</Text>
</SliderControl>
<Animated.View
style={
{
transform: [{
translateX: interpolatedAnimation,
}],
}
}
>
{renderContent()}
</Animated.View>
<SliderControl pressHandler={loadNextItems}>
<Text>{'>'}</Text>
</SliderControl>
</View>
The slider controls are tied to onPress event handlers. The handlers adjusts the animation value as needed. When the slider is re-rendered, it is animated (slide to left/right) and shows next/prev items. I figured that they are using transform translateX animation for this. This is easily achievable with Animated API
const animation = useRef(new Animated.Value(0)).current;
const interpolatedAnimation = useMemo(() => {
return animation.interpolate({
inputRange: [0, 100],
outputRange: [0, sliderWidth],
});
}, [animation, sliderWidth]);
useEffect(() => {
Animated.timing(animation, {
toValue: moveDirection === 'right ? -100 : 100,
duration: 750,
useNativeDriver: true,
}).start();
});
Along with the Next/Prev button, Netflix slider also supports the trackpad scroll events. If we scroll slightly, the slider doesn't move at all. But we do a momentum scroll, it slides exactly 6 (itemsPerRow) items.
A general View doesn't support scroll event handlers to listen to trackpad scroll action. Not sure how I can achieve this part
ScrollView - lot of unwanted scrolling
View - No support for scroll event listener
Any ideas?

Why useState hook doesn't update in WebView onScroll event?

I'm trying to create a workaround to calculate/get content height which should be rendered inside React Native WebView component.
For that Im artificially calling onScroll event with Javascript , then trying to update state with the actual content height. But even console log shows different values for content height, which should trigger state update , it doesnt happen. Only when really scrolling it gets updated. But its for sure that even without scrolling the event is triggered as console log is working.
Any idea why here state update fails? (In the code snippet some backticks are missing)
const [contentHeight, setContentHeight] = useState(10);
const webViewScript =
setTimeout(function() {
window.postMessage(document.body.scrollDown);
}, 1000);
true; // note: this is required, or you'll sometimes get silent failures
;
<WebView
injectedJavaScript={window.ReactNativeWebView.postMessage(${webViewScript})}
domStorageEnabled={true}
automaticallyAdjustContentInsets={false}
onScroll={(event) =>
setContentHeight(Number(event.nativeEvent.contentSize.height));
}
javaScriptEnabled
style={{ height: contentHeight }}
source={{ html: htmlTempl, baseUrl: '' }}
/>

Bidirectional infinite scrolling with FlatList?

Has anyone had experience doing bidirectional infinite scrolling with FlatList (or any other component)?
My use case is I have a calendar month view that I want to be able to swipe left and right to get the next and previous month.
FlatList with onEndReached and onEndReachedThreshold works great for one direction, but there is no similar thing for the start. Any suggestions?
You can use the onScroll props on the Flatlist for that: https://reactnative.dev/docs/scrollview#onscroll
onScroll={({
nativeEvent: {
contentInset: { bottom, left, right, top },
contentOffset: { x, y },
contentSize: { height: contentHeight, width: contentWidth },
layoutMeasurement: { height, width },
zoomScale,
},
}) => {
if ( y <= your_start_threshold) {
// Do what you need to do on start reached
}
}}
Only problem is that React native doesn't support bidirectional FlatList on Android. This means that when you add items to the start of your list, it will jump to the top instead of keeping the current items in view (https://github.com/facebook/react-native/issues/25239).
(iOs does have a maintainVisibleContentPosition props)

How to set fixed position the react native action button on the screen

Can I set the position of react native action button to fixed on a flat list?
I have added an action button but when I scroll the flat list, the action button hides!
Thanks, advance.
You'll want to use the Animated API
First you need to create a Flatlist that you can animate. This is easily done with Animated.createAnimatedComponent():
import { Flatlist, Animated } from 'react-native'
const AnimatedFlatlist = Animated.createAnimatedComponent(Flatlist)
Now in your parent component constructor create a new Animated.Value. This will be the value you use to animate your component to scroll along with your AnimatedFlatlist:
constructor(props) {
super(props)
this.state = { translateY: new Animated.Value(0)}
}
Then wrap the component you want to be fixed in an Animated.View and make sure you add a transform to the component style, passing it the animated value you created :
<Animated.View style={{ transform: [{ translateY: this.state.translateY }]}}>
{YourFixedComponent}
</Animated.View>
Finally, you need to attach an Animated.event to your AnimatedFlatlist. This is done using the onScroll prop:
<AnimatedFlatlist
data={...}
renderItem={...}
keyExtractor={...}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: this.state.translateY}}}],
{useNativeDriver: true}
)}
scrollEventThrottle={1}
/>
Now your element with move along with your Flatlist. This is a basic example, and you will probably need to mess with interpolation and CSS positioning to get it exactly as you want it.