Hello there!
My name is Alex, and I am a mobile software engineer with professional experience in iOS and React Native development. I'd like to share with you a story of creating my first open-source library for React Native.
In one of my projects, I needed a custom view which will stick to the keyboard top when it opens. Usually, it is an easy thing to do - you just listen to the keyboard open/close events and adjust your view position accordingly. However, when you use it with a scroll view which has keyboardDismissMode
set to interactive
, and you try to dismiss the keyboard interactively with a drag gesture, this view will stay in a position of the open keyboard until it is fully closed. I wanted it to move in synchrony with the touch as I am used to experiencing in the native iOS apps.
Later, I found an InputAccessoryView
component in React Native and after adding it - bingo! The view was following the keyboard flawlessly. I thought the case was closed and moved on until I noticed two weird bugs. Firstly, the content of the accessory view isn't resized after phone orientation change. I have posted an issue here. Secondly, the input accessory simply disappears every time you present a modal view. There is a stale issue regarding that, so this was never resolved. This is a moment where I've decided to write my own solution.
Solution
As I mentioned before, if we don't want interactive dismiss support on iOS, it is enough to listen to the keyboard open/close events, in my case keyboardWillChangeFrame
event, because it is available on both Android and iOS. The only thing left is to track a finger's position on a screen and if it's Y value will be higher than keyboard's top line, change the bottom value of the accessory view to it. Fortunately, we have something called PanResponder in React Native. So I wrote this simple hook:
export const usePanResponder = () => {
const [positionY, setPositionY] = React.useState(0)
const panResponder = React.useRef(
PanResponder.create({
onPanResponderMove: (_, gestureState) => {
setPositionY(gestureState.moveY)
},
onPanResponderEnd: () => {
setPositionY(0)
},
})
).current
return {
panHandlers: Platform.OS === 'android' ? {} : panResponder.panHandlers,
positionY,
}
}
As you can see, on Android panHandlers
is an empty object, because there is no interactive dismiss. Then we need to destructure panHandlers
on our scrollable component and pass the provided positionY
to the KeyboardAccessoryView
.
Another important part is to offset scrollable content accordingly and to do that, aside from the keyboard dimensions, we also need a height of the accessory view. This height can be dynamic (e.g. multiline text input), so another simple hook was created, which provides a dynamic size value:
export const useComponentSize = () => {
const [size, setSize] = React.useState({ height: 0, width: 0 })
const onLayout = React.useCallback((event: LayoutChangeEvent) => {
const { height, width } = event.nativeEvent.layout
setSize({ height, width })
}, [])
return { onLayout, size }
}
Based on the accessory view size and keyboard dimensions, KeyboardAccessoryView
component provides an onContentBottomInsetUpdate
callback, which can be used to adjust a content offset.
Bonus
I am using react-native-testing-library
for testing purposes. One question I had is how can I trigger the keyboardWillChangeFrame
event to test my code. Turns out it is as simple as making a mock of the NativeEventEmitter
in your jest setup file. And later in test files, you can emit different events:
// Jest setup
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter')
// Test file
import { NativeEventEmitter } from 'react-native'
import { act } from 'react-native-testing-library'
const emitter = new NativeEventEmitter()
...
act(() => {
emitter.emit('keyboardWillChangeFrame', keyboardOpenEvent)
emitter.emit('didUpdateDimensions', {
screen: scaledSize,
window: scaledSize,
})
})
I hope you learned something new from this article, and thanks for the reading! The final result can be found here https://github.com/flyerhq/react-native-keyboard-accessory-view
Top comments (2)
Hey @demchenkoalex I was wondering why you're not using the Animated API on your library.
Thanks
Hi, updated today, thanks for the suggestion :)