Skip to content

Commit 26358d5

Browse files
author
Manisha choudhary
committed
fix(menu): account for keyboard already open when Menu mounts
Use Keyboard.metrics() when measuring layout so a Menu that mounts while the keyboard is visible still avoids it. Fixes #5096.
1 parent 31d8c58 commit 26358d5

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

src/components/Menu/Menu.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ const EASING = Easing.bezier(0.4, 0, 0.2, 1);
117117

118118
const WINDOW_LAYOUT = Dimensions.get('window');
119119

120+
const getKeyboardHeight = (fallback: number) =>
121+
Keyboard.metrics()?.height ?? fallback;
122+
120123
const DEFAULT_ELEVATION: Elevation = 2;
121124
const DEFAULT_MODE = 'elevated';
122125

@@ -380,7 +383,9 @@ const Menu = ({
380383
});
381384

382385
setWindowLayout({
383-
height: windowLayoutResult.height - keyboardHeightRef.current,
386+
height:
387+
windowLayoutResult.height -
388+
getKeyboardHeight(keyboardHeightRef.current),
384389
width: windowLayoutResult.width,
385390
});
386391

@@ -461,6 +466,8 @@ const Menu = ({
461466
'keyboardDidHide',
462467
keyboardDidHide
463468
);
469+
// iOS does not replay keyboardDidShow for a keyboard that is already open.
470+
keyboardHeightRef.current = getKeyboardHeight(keyboardHeightRef.current);
464471

465472
return () => {
466473
removeListeners();

src/components/__tests__/Menu.test.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Dimensions, StyleSheet, View } from 'react-native';
1+
import { Dimensions, Keyboard, StyleSheet, View } from 'react-native';
22

33
import { expect, it, jest } from '@jest/globals';
44
import { act, screen, waitFor } from '@testing-library/react-native';
@@ -234,3 +234,70 @@ it('renders menu with mode "flat"', async () => {
234234
expect(styles).not.toHaveProperty('shadowColor');
235235
expect(styles).not.toHaveProperty('shadowOpacity');
236236
});
237+
238+
it('accounts for a keyboard that is already open when the menu mounts', async () => {
239+
const testID = 'keyboard-aware-menu';
240+
const dimensionsSpy = jest.spyOn(Dimensions, 'get').mockReturnValue({
241+
width: 400,
242+
height: 800,
243+
scale: 2,
244+
fontScale: 2,
245+
});
246+
const keyboardMetricsSpy = jest.spyOn(Keyboard, 'metrics').mockReturnValue({
247+
screenX: 0,
248+
screenY: 500,
249+
width: 400,
250+
height: 300,
251+
});
252+
253+
let measureCalls = 0;
254+
const measureSpy = jest
255+
.spyOn(View.prototype, 'measureInWindow')
256+
.mockImplementation((fn) => {
257+
measureCalls += 1;
258+
if (measureCalls % 2 === 1) {
259+
// Menu content is tall enough to overflow the remaining window.
260+
fn(100, 100, 200, 400);
261+
} else {
262+
fn(100, 100, 80, 32);
263+
}
264+
});
265+
266+
function makeMenu(visible: boolean) {
267+
return (
268+
<Portal.Host>
269+
<Menu
270+
visible={visible}
271+
onDismiss={jest.fn()}
272+
anchor={
273+
<Button mode="outlined" testID="anchor">
274+
Open menu
275+
</Button>
276+
}
277+
testID={testID}
278+
>
279+
<Menu.Item onPress={jest.fn()} title="Undo" />
280+
<Menu.Item onPress={jest.fn()} title="Redo" />
281+
</Menu>
282+
</Portal.Host>
283+
);
284+
}
285+
286+
const { rerender } = await render(makeMenu(false));
287+
288+
await act(async () => {
289+
await rerender(makeMenu(true));
290+
await Promise.resolve();
291+
});
292+
293+
await waitFor(() => {
294+
// eslint-disable-next-line no-restricted-syntax -- layout height is not otherwise exposed.
295+
const styles = StyleSheet.flatten(screen.getByTestId(testID).props.style);
296+
// Available height is window (800) minus keyboard (300) minus top (100) minus SCREEN_INDENT (8).
297+
expect(styles).toEqual(expect.objectContaining({ height: 392 }));
298+
});
299+
300+
measureSpy.mockRestore();
301+
keyboardMetricsSpy.mockRestore();
302+
dimensionsSpy.mockRestore();
303+
});

0 commit comments

Comments
 (0)