Using useRef to make a tap on a container ScrollView put focus in a TextInput - react-native

This is for a React Native app.
One of my screens has a ScrollView wrapped around a TextInput. The ScrollView has a lot of height - moreso than the TextInput when it's blank - so the behavior I want is that if a user taps on any of the 'blank space' in the ScrollView, it will put focus in the TextInput.
The code I have so far boils down to this:
export function MainInput() {
const ref_textinput = useRef();
const onTapScrollViewBody = () => {
console.log("Detected tap")
ref_textinput.current?.focus()
}
return (
<TouchableWithoutFeedback onPress={onTapScrollViewBody}>
<ScrollView style={styles.parentScrollView}>
<TextInput
ref={ref_textinput}
...
It's not working unfortunately. When focus isn't in the TextInput, the first time I tap in the blank space of the ScrollView, I do see the console log, but focus doesn't go into the TextInput, nor do further taps on the blank space trigger more console logs. However, tapping into the TextInput (which obviously puts focus back on it) then tapping back out will trigger the console log.
Am I misunderstanding how useRef works here?

Your understanding of useRef is fine, it's the understanding of the ScrollView that needs some help. If two elements that share coordinates are touchable, React Native will give the touch to the "top" element (whether by z-index, render order, etc). Your example creates the following hierarchy:
|-TouchableWithoutFeedback
|-ScrollView
|-TextInput
If you press within the TextInput's area, it will always capture the touch, as you found. If you press within the ScrollView's area, but outside of the text input, your touch is captured by the ScrollView, which will try to use your touch to scroll it. Only when you touch outside the ScrollView should your TouchableWithoutFeedback activate.
You still want the ScrollView to scroll, so when do you want your tap to focus the text input? You could delete your Touchable and use an event exposed by ScrollView, like
<ScrollView
onScrollEndDrag={() => ref_textinput.current?.focus()}
// and/or
onMomentumScrollEnd={() => ref_textinput.current?.focus()}
...
A solution that would handle tapping differently from scrolling could be achieved using react-native-gesture-handler, but it would be more involved.

Related

How can I use react-native-gesture-handler to handle react-native-skia touches inside a ScrollView?

I have a ScrollView containing several graphs made with react-native-skia. The graphs are interactable, i.e. I can touch them and move an indicator on the graph along the x-axis of the graph.
My issue is that the whenever the ScrollView events fire (i.e. we scroll up/down), then the graph touch events are ignored which makes for bad UX.
Demo:
Here's a Snack with a reproducible demo: https://snack.expo.dev/#jorundur/supportive-raisins
FYI: For some reason for me the Canvas disappears after 1 second in the Snack, but if you make any change in the file and save (such add a newline somewhere), then it appears. It's fine if you run this locally. But that's some other problem we can ignore for the purpose of this discussion.
Description:
The demo is a react-native ScrollView large enough to make sure we have to scroll, the top component is a graph using react-native-skia. If we drag the cursor in the graph around, then the UX gets bad pretty quickly as the graph touch events seem to be ignored as soon as any vertical scrolling happens. I've tried playing around with ScrollView from react-native-gesture-handler but without luck.
To me, the expected behaviour would be for the graph to be interactable even while scrolling. I.e. if I'm pressing the graph and move my finger diagonally up/down I would expect the ScrollView to scroll and the graph cursor also to change its position accordingly. (I say diagonally since a straight vertical movement wouldn't change the cursor position in this graph.)
Much appreciated if anyone has any ideas on how to solve this! I couldn't work out how to do it via GestureDetector from react-native-gesture-handler like I've heard suggested.
Possible solution (?):
What I think I need to do is remove the onTouch={touchHandler} which I'm using currently in the react-native-skia Canvas component and instead get those touches via gesture detection from react-native-gesture-handler. I then need to make those gestures work simultaneously with the parent ScrollViews scroll event gestures. I've not had any luck implementing that unfortunately.
The solution was to do the following:
Don't use onTouch on Canvas, instead detect gestures via react-native-gesture-handler
Create a gesture and add a ref to it
Add the simultaneousHandlers prop to the ScrollView and use the ref there
This tells the ScrollView that its events should not be blocked by the touch events from the ref
To reiterate, what I wanted to do was to have the touch events of a ScrollView work simultaneously with touch events from a react-native-skia Canvas child component.
Code (relevant bits):
import * as React from 'react';
import {
GestureHandlerRootView,
ScrollView // Note that this is not imported from react-native
} from 'react-native-gesture-handler';
const App = () => {
// Create ref for the ScrollView to know what other gestures it should work simultaneously with.
// My use case required pan but this can be swapped for other gestures.
const panGestureRef = React.useRef<GestureType>(Gesture.Pan());
// The logic that used to be in `onTouch` on the Canvas is now here
const panGesture = Gesture.Pan()
.onChange((e) => {
// Do stuff, also use the other callbacks available as needed
})
.withRef(panGestureRef);
return (
<GestureHandlerRootView style={{ flex: 1 }}>{/* Don't forget this! It should be at the root of your project. */}
<ScrollView simultaneousHandlers={[panGestureRef]}>
<GestureDetector gesture={panGesture}>
<Canvas style={{ width: 200, height: 200 }}>
{/* Your interactive react-native-skia magic */}
</Canvas>
</GestureDetector>
</ScrollView>
</GestureHandlerRootView>
);
};
export default App;

React Native handle touch release

I have a component which when the user long press a card I show a bigger version of this card.
The ideia is that the bigger card will be shown as long as the user keep pressing the touch and then will hide only when the finger is released (something like instagram long press). I tried to archieve this using the onLongPress and the onPressOut props of <TouchableHighlight>, the thing is that the onPressOut props has something that they call "cancel",
/**
* Called when the touch is released,
* but not if cancelled (e.g. by a scroll that steals the responder lock).
*/
What is happening is that when the user hold and move the finger the onPressOut prop is called, therefore the bigger card is hidden.
This is the code:
<View style={styles.container}>
<View style={styles.separator}>
<TouchableHighlight
underlayColor="rgba(255, 255, 255, 0)"
onPress={this.cardClick}
onLongPress={this.cardLongPress}
onPressOut={this.cardPressOut}
>
{this.content}
</TouchableHighlight>
</View>
</View>
Here is a GIF to show what is happening:
GIF
What I want is something that is only triggered when the user acctually releases his finger, regardless of whether or not he is moving the finger arround. Thanks in advance for the help.
Try setting an offset https://facebook.github.io/react-native/docs/touchablewithoutfeedback#pressretentionoffset , or convert your root view in a touchablewithoutfeedback, and call onPressOut there
So you want an Instagram style preview modal. I got you.
As mentioned in previous comments, you should use the pressRetentionOffset prop that will let you "extend" the pressable area. https://facebook.github.io/react-native/docs/touchablewithoutfeedback#pressretentionoffset
But! this prop will only work if the ScrollView is disabled. so you will need to disable the scrolling when the preview modal is shown.
You can do that with the scrollEnabled prop on ScrollView and make it falsy when the preview modal is shown.
Of course, this works with onLongPress and onPressOut props.

react native: double tap needed for ScrollView / FlatList / SectionList?

If I have data in a ScrollView, FlatList, or SectionList, and that data includes a button that the user can press on, tapping on the button once hides the Keyboard as expected:
onScroll={() => Keyboard.dismiss()}
but it does not trigger the button callback. It only works if you tap the same button a second time after the keyboard is hidden. Is there any way to fix this?
I figured out the answer from the docs, setting keyboardShouldPersistTaps="always"

ScrollView only scrolls when placing finger inside of an input

I'm having a weird behavior here and I can't figure out what's going on. I have a ScrolView with a form and some inputs and labels inside and it seems to only want to scroll when you place your finger (or cursor in the demo) over an input or a switch and it doesn't scroll when placing your finger anywhere else in the ScrollView
I put together an Expo Snack to show the code and if you run in the emulator and attempt to scroll the ScrollView by placing the cursor over one of the labels or on the edges of the ScrollView it won't scroll but if you place the cursor over an input or a switch it scrolls just fine
https://snack.expo.io/#jordanr/weird-scrollview-bahavior
The issue is because Touchable effects in your TouchableWithoutFeedback are blocking the effects of ScrollView, therefore you need to reset your responder by wrapping the Content inside the View
<Content>
<View onStartShouldSetResponder={() => true}>
//... Rest of the code
</View>
</Content>
Also don't use ScrollView since NativeBase uses KeyboardAwareScrollView from the package react-native-keyboard-aware-scroll-view in the <Content/>

Ignore rest of screen when keyboard active

So i am adding the final touches to my application, and one bug that has been present for a while is when they keyboard is active, i am able to use the navigation on the upper half of the screen with the keyboard still showing
Is there a simple way to dismiss all other touches on the upper half of the screen when the keyboard is active? If you could point me in the right direction that would be great.
Looking for something similar to this: When keyboard active then touch screen anywhere, close keyboard
EDIT: would be nice if the textInput that the keyboard is outputting to was still touchable
If the parent view of the TextInput is a ScrollView, then u can use the prop 'keyboardShouldPersistTaps',
<ScrollView keyboardShouldPersistTaps='handled' />
If you're using a View and dont want scrolling, then probably u can wrap it inside a
//This should disable scroll
<ScrollView scrollEnabled={false} >
Documentation for keyboardShouldPersistTaps (React Native website)
keyboardShouldPersistTaps?: enum('always', 'never', 'handled', false,
true) #
Determines when the keyboard should stay visible after a tap.
'never' (the default), tapping outside of the focused text input when
the keyboard is up dismisses the keyboard. When this happens, children
won't receive the tap. 'always', the keyboard will not dismiss
automatically, and the scroll view will not catch taps, but children
of the scroll view can catch taps. 'handled', the keyboard will not
dismiss automatically when the tap was handled by a children, (or
captured by an ancestor). false, deprecated, use 'never' instead true,
deprecated, use 'always' instead