Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions apps/mobile/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
// app, NOT the Ungoogled Chromium engine — see docs/mobile-architecture.md.
import { StatusBar } from 'expo-status-bar';
import { useState, type ReactNode } from 'react';
import { Keyboard, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import {
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { BrowserScreen } from './src/screens/BrowserScreen';
import { ChatScreen } from './src/screens/ChatScreen';
Expand Down Expand Up @@ -57,7 +65,12 @@ function AppShell() {
const insets = useSafeAreaInsets();

return (
<View
<KeyboardAvoidingView
// Edge-to-edge Android windows do not reliably resize the absolute tab
// scenes for the IME. Resize their shared root, including the tab bar.
enabled={Platform.OS === 'android'}
behavior={Platform.OS === 'android' ? 'height' : undefined}
keyboardVerticalOffset={0}
style={[
styles.root,
{ paddingTop: insets.top, paddingLeft: insets.left, paddingRight: insets.right },
Expand Down Expand Up @@ -98,7 +111,7 @@ function AppShell() {
);
})}
</View>
</View>
</KeyboardAvoidingView>
);
}

Expand Down
42 changes: 42 additions & 0 deletions apps/mobile/test/keyboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import App from '../App';
import { flat, hosts, renderScreen, scenes, tabButton } from './harness';
import { Platform } from './mocks/react-native';
import { setMockSafeAreaInsets } from './mocks/react-native-safe-area-context';

// Native geometry is exercised separately with the APK. These tests pin the
// platform wiring so mocked native components cannot claim to prove IME layout.
describe('shell keyboard avoidance', () => {
it('resizes the Android shell containing the scenes and tabs', async () => {
const renderer = await renderScreen(<App />);
const shell = hosts(renderer.root, 'KeyboardAvoidingView').find(
(node) => node.props.enabled === true,
);
expect(shell).toBeDefined();
expect(shell!.props.behavior).toBe('height');
expect(shell!.props.keyboardVerticalOffset).toBe(0);
expect(scenes(shell!)).toHaveLength(4);
expect(tabButton(shell!, 'Chat')).toBeDefined();
});

it('keeps system insets inside the keyboard-aware root without a second offset', async () => {
setMockSafeAreaInsets({ top: 37, right: 12, bottom: 48, left: 12 });
const renderer = await renderScreen(<App />);
const shell = hosts(renderer.root, 'KeyboardAvoidingView').find(
(node) => node.props.enabled === true,
);
expect(shell).toBeDefined();
expect(flat(shell!)).toMatchObject({ paddingTop: 37, paddingLeft: 12, paddingRight: 12 });
expect(shell!.props.keyboardVerticalOffset).toBe(0);
});

it('does not add a second keyboard adjustment to the existing iOS chat', async () => {
Platform.OS = 'ios';
const renderer = await renderScreen(<App />);
const views = hosts(renderer.root, 'KeyboardAvoidingView');
const shell = views.find((node) => node.props.enabled === false);
expect(shell).toBeDefined();
expect(shell!.props.behavior).toBeUndefined();
expect(views.some((node) => node.props.behavior === 'padding')).toBe(true);
});
});
2 changes: 1 addition & 1 deletion apps/mobile/test/safe-area.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { setMockSafeAreaInsets } from './mocks/react-native-safe-area-context';

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

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

const tabBar = (root: ReactTestInstance) =>
hostWhere(
Expand Down
Loading