Skip to content

Commit fabddbf

Browse files
Keep Android chat controls above the keyboard
1 parent b99c007 commit fabddbf

3 files changed

Lines changed: 59 additions & 4 deletions

File tree

‎apps/mobile/App.tsx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
// app, NOT the Ungoogled Chromium engine — see docs/mobile-architecture.md.
44
import { StatusBar } from 'expo-status-bar';
55
import { useState, type ReactNode } from 'react';
6-
import { Keyboard, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
6+
import {
7+
Keyboard,
8+
KeyboardAvoidingView,
9+
Platform,
10+
StyleSheet,
11+
Text,
12+
TouchableOpacity,
13+
View,
14+
} from 'react-native';
715
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
816
import { BrowserScreen } from './src/screens/BrowserScreen';
917
import { ChatScreen } from './src/screens/ChatScreen';
@@ -57,7 +65,12 @@ function AppShell() {
5765
const insets = useSafeAreaInsets();
5866

5967
return (
60-
<View
68+
<KeyboardAvoidingView
69+
// Edge-to-edge Android windows do not reliably resize the absolute tab
70+
// scenes for the IME. Resize their shared root, including the tab bar.
71+
enabled={Platform.OS === 'android'}
72+
behavior={Platform.OS === 'android' ? 'height' : undefined}
73+
keyboardVerticalOffset={0}
6174
style={[
6275
styles.root,
6376
{ paddingTop: insets.top, paddingLeft: insets.left, paddingRight: insets.right },
@@ -98,7 +111,7 @@ function AppShell() {
98111
);
99112
})}
100113
</View>
101-
</View>
114+
</KeyboardAvoidingView>
102115
);
103116
}
104117

‎apps/mobile/test/keyboard.test.tsx‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, it } from 'vitest';
2+
import App from '../App';
3+
import { flat, hosts, renderScreen, scenes, tabButton } from './harness';
4+
import { Platform } from './mocks/react-native';
5+
import { setMockSafeAreaInsets } from './mocks/react-native-safe-area-context';
6+
7+
// Native geometry is exercised separately with the APK. These tests pin the
8+
// platform wiring so mocked native components cannot claim to prove IME layout.
9+
describe('shell keyboard avoidance', () => {
10+
it('resizes the Android shell containing the scenes and tabs', async () => {
11+
const renderer = await renderScreen(<App />);
12+
const shell = hosts(renderer.root, 'KeyboardAvoidingView').find(
13+
(node) => node.props.enabled === true,
14+
);
15+
expect(shell).toBeDefined();
16+
expect(shell!.props.behavior).toBe('height');
17+
expect(shell!.props.keyboardVerticalOffset).toBe(0);
18+
expect(scenes(shell!)).toHaveLength(4);
19+
expect(tabButton(shell!, 'Chat')).toBeDefined();
20+
});
21+
22+
it('keeps system insets inside the keyboard-aware root without a second offset', async () => {
23+
setMockSafeAreaInsets({ top: 37, right: 12, bottom: 48, left: 12 });
24+
const renderer = await renderScreen(<App />);
25+
const shell = hosts(renderer.root, 'KeyboardAvoidingView').find(
26+
(node) => node.props.enabled === true,
27+
);
28+
expect(shell).toBeDefined();
29+
expect(flat(shell!)).toMatchObject({ paddingTop: 37, paddingLeft: 12, paddingRight: 12 });
30+
expect(shell!.props.keyboardVerticalOffset).toBe(0);
31+
});
32+
33+
it('does not add a second keyboard adjustment to the existing iOS chat', async () => {
34+
Platform.OS = 'ios';
35+
const renderer = await renderScreen(<App />);
36+
const views = hosts(renderer.root, 'KeyboardAvoidingView');
37+
const shell = views.find((node) => node.props.enabled === false);
38+
expect(shell).toBeDefined();
39+
expect(shell!.props.behavior).toBeUndefined();
40+
expect(views.some((node) => node.props.behavior === 'padding')).toBe(true);
41+
});
42+
});

‎apps/mobile/test/safe-area.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { setMockSafeAreaInsets } from './mocks/react-native-safe-area-context';
1111

1212
import type { ReactTestInstance } from 'react-test-renderer';
1313

14-
const shellRoot = (root: ReactTestInstance) => hosts(root, 'View')[0];
14+
const shellRoot = (root: ReactTestInstance) => hosts(root, 'KeyboardAvoidingView')[0];
1515

1616
const tabBar = (root: ReactTestInstance) =>
1717
hostWhere(

0 commit comments

Comments
 (0)