callstack / callstack/react-native-paper
Menu ignores the keyboard when it mounts while the keyboard is already open
- 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
Assessment
This issue has not been assessed yet.