callstack / callstack/react-native-paper

Menu ignores the keyboard when it mounts while the keyboard is already open

Open
#5,096 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
14.5k
Forks
2.2k
Avg merge
5d 23h
Merged PRs (30d)
12

Description

### Summary

`Menu` accounts for the on-screen keyboard, but it learns the keyboard height **only from a `keyboardDidShow` event that arrives after it mounts**. iOS does not replay that event for a keyboard that is already open, so a `Menu` that **mounts while the keyboard is already up** keeps a height of `0`, measures against the full window, and lays its items out underneath the keyboard — where they cannot be tapped, and where its own overflow scrolling never engages, because as far as it knows the items fit.

### Current behaviour

`src/components/Menu/Menu.tsx` in 5.14.5:

- L215 — `const keyboardHeightRef = React.useRef(0);`
- L221-228 — `keyboardDidShow` / `keyboardDidHide` set and clear that ref.
- L413-420 — both listeners are attached inside a **mount** effect.
- L343 — `show()` uses the ref: `height: windowLayoutResult.height - keyboardHeightRef.current`

When the keyboard is already visible at mount, no `keyboardDidShow` arrives, the ref stays `0`, and L343 subtracts nothing.

### Why this shows up in practice

Any menu that belongs to a row created while the user is typing. In our app a text field searches for people, and each person added brings a row with its own `Menu`. The field keeps focus (`keyboardShouldPersistTaps="handled"`), so every one of those menus mounts with the keyboard already open.

Two menus on one screen, same bundle and same keyboard state, made the cause unambiguous:

| Menu | Mounts | Keyboard height it believes | Result |
|---|---|---|---|
| Rendered with the screen | Before any keyboard | correct | 9 items, correctly placed clear of the keyboard |
| Rendered into a row added while typing | Keyboard already open | `0` | Ran to y=737 with the keyboard top at y=509; the last 6 items were untappable |

### Steps to reproduce

1. Render a `TextInput` inside a `ScrollView` with `keyboardShouldPersistTaps="handled"`.
2. Render a `Menu` **conditionally**, so that it mounts only after the input has text — for example `{value.length > 0 && }`.
3. Give the menu enough items to reach past the keyboard (about 8 on a 390×844 screen).
4. Focus the input, type a character, then tap the anchor without dismissing the keyboard.

The menu extends under the keyboard. Mount the same menu unconditionally and it positions correctly.

### Expected behaviour

The menu measures against the space the keyboard leaves, whenever it mounted.

### Suggested fix

Read the current keyboard metrics at open time instead of relying only on events. In `show()`:

```diff
setWindowLayout({
- height: windowLayoutResult.height - keyboardHeightRef.current,
+ height:
+ windowLayoutResult.height -
+ (Keyboard.metrics()?.height ?? keyboardHeightRef.current),
width: windowLayoutResult.width,
});
```

`Keyboard.metrics()` returns `undefined` when the keyboard is not shown, so the existing ref stays as the fallback.

**I have not tested this patch.** I worked around the problem in application code instead, and I have not checked how `Keyboard.metrics()` behaves on Android.

### Workaround

Call `Keyboard.dismiss()` before opening the menu. That makes the component's assumption true. It costs nothing in our case, because choosing from a menu is not typing.

### Environment

- react-native-paper 5.14.5
- react-native 0.81.5
- Expo SDK 54
- iOS 26.2, iPhone 16e simulator

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.