Making a right keyboard accessory view in React Native
Tags: reactnative, typescript, ios, android
Published: 2020-06-12 08:23:30

![Keyboard accessory view demo](https://dev-to-uploads.s3.amazonaws.com/i/xghk64pp4rn7sg2v76lj.gif)

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](https://github.com/facebook/react-native/issues/27887). 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](https://reactnative.dev/docs/panresponder) in React Native. So I wrote this simple hook:
```typescript
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:
```typescript
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`](https://github.com/callstack/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:
```typescript
// 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