loading...

Making a right keyboard accessory view in React Native

demchenkoalex profile image Alex ・3 min read

Keyboard accessory view demo

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

Posted on by:

Discussion

markdown guide
 

Hey @demchenkoalex I was wondering why you're not using the Animated API on your library.

Thanks

 

Hi, updated today, thanks for the suggestion :)