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
21 changes: 21 additions & 0 deletions src/components/DrawerPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { View, type StyleProp, type ViewStyle } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

/**
* The inside of a side drawer, kept clear of the status bar, the home
* indicator and the left edge. The padding comes from the provider's insets,
* which are known on the first render (a new provider starts with its
* parent's). The native SafeAreaView only learned them a few frames later on
* Fabric, and every time the drawer opened: its header first drew under the
* status bar or Dynamic Island and then jumped down, while the drawer was
* still sliding in.
*/
export function DrawerPanel({ style, children }: { style?: StyleProp<ViewStyle>; children: React.ReactNode }) {
const insets = useSafeAreaInsets();
return (
<View style={[style, { paddingTop: insets.top, paddingBottom: insets.bottom, paddingLeft: insets.left }]}>
{children}
</View>
);
}
3 changes: 2 additions & 1 deletion src/components/SafeAreaModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
* insets: the view starts off screen. On iPhones with a notch or Dynamic
* Island headers (and their close buttons) ended up under the status bar,
* where taps don't reach them, and bottom rows on the home indicator. Use
* this for any Modal with a SafeAreaView inside.
* this for any Modal with a SafeAreaView inside, or with content that reads
* the insets of the Modal's window (the side drawers' DrawerPanel).
*/
export function SafeAreaModal({ children, ...props }: ModalProps) {
return (
Expand Down
11 changes: 5 additions & 6 deletions src/components/SidebarDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Animated, Dimensions, Easing, ActivityIndicator,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
Inbox, Send, File as FileIcon, Trash2, Ban, Archive, Star,
Folder, FolderOpen, ChevronDown, ChevronRight, X, Settings, LogOut, Check, Plus,
Expand Down Expand Up @@ -46,6 +45,7 @@ import type { Mailbox } from '../api/types';
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
import { confirmSignOut } from '../lib/sign-out-confirm';
import { SafeAreaModal } from './SafeAreaModal';
import { DrawerPanel } from './DrawerPanel';
import { setPendingSettingsTab } from '../navigation/pending-settings-tab';
import {
isAccountMuted, isMailboxMuted, mailboxOverridesForAccount, resetMailboxMutes, setAccountMuted, setMailboxMuted,
Expand Down Expand Up @@ -881,7 +881,7 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
const tagViewActive = !!filters.keyword;

const panel = (
<SafeAreaView style={styles.drawerSafe} edges={['top', 'bottom', 'left']}>
<DrawerPanel style={styles.drawerSafe}>
{/* Header: close + account switcher */}
<View style={styles.header}>
{!docked && (
Expand Down Expand Up @@ -1311,7 +1311,7 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
</>
)}
</ScrollView>
</SafeAreaView>
</DrawerPanel>
);

// On a wide screen the drawer is a permanent sidebar next to the list.
Expand All @@ -1335,9 +1335,8 @@ export default function SidebarDrawer({ visible, onClose, docked = false }: Side
);
}

// Its own safe-area provider: the panel's SafeAreaView starts off screen
// (it slides in), and without one it measured no insets on iOS, so the
// account header sat under the status bar.
// The Modal is its own window, with its own safe-area provider; the panel
// (DrawerPanel) pads itself from that provider's insets.
return (
<SafeAreaModal supportedOrientations={MODAL_ORIENTATIONS}
visible={visible}
Expand Down
21 changes: 19 additions & 2 deletions src/components/__tests__/modal-safe-area.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ describe('modals that slide in with a SafeAreaView', () => {
expect(offenders).toEqual([]);
});

// The mail drawer builds its panel (with the SafeAreaView) outside the
// modal's tags, so the check above can't see it there.
// The drawers' panels read their insets from the modal's own provider
// (DrawerPanel, below), so they need SafeAreaModal as well.
it('include the side drawers', () => {
const drawers = sourceFiles(SRC).filter((file) => /SidebarDrawer\.tsx$/.test(file));
expect(drawers.length).toBeGreaterThanOrEqual(3);
Expand All @@ -78,6 +78,23 @@ describe('modals that slide in with a SafeAreaView', () => {
}
});

// #130: inside the provider the native SafeAreaView still learned the
// insets a few frames late, each time a drawer opened, so its header jumped
// down while the drawer slid in. DrawerPanel pads from the provider's
// insets, which are there on the first render.
it('pad the side drawers from the insets, not with a native SafeAreaView', () => {
const drawers = sourceFiles(SRC).filter((file) => /SidebarDrawer\.tsx$/.test(file));
for (const file of drawers) {
const source = fs.readFileSync(file, 'utf8');
expect(source, path.relative(SRC, file)).not.toContain('<SafeAreaView');
expect(source, path.relative(SRC, file)).toContain('<DrawerPanel style={styles.drawerSafe}>');
}
const panel = fs.readFileSync(path.join(SRC, 'components/DrawerPanel.tsx'), 'utf8');
expect(panel).toContain('const insets = useSafeAreaInsets();');
expect(panel).toContain('paddingTop: insets.top, paddingBottom: insets.bottom, paddingLeft: insets.left');
expect(panel).not.toContain('<SafeAreaView');
});

it('finds the tags it is looking for', () => {
const tags = modalTags('<View><Modal visible={x} onRequestClose={() => close()} animationType="slide">\n</Modal></View>');
expect(tags).toHaveLength(1);
Expand Down
6 changes: 3 additions & 3 deletions src/components/calendar/CalendarSidebarDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
Text,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
Check, X, Upload, Rss, Shuffle, Star, Plus, Pencil, Share2, Eraser, Trash2, RefreshCw, ArrowUp, ArrowDown,
type AppIcon,
Expand All @@ -24,6 +23,7 @@ import { isWritableCalendar } from '../../lib/calendar-editability';
import { useLocaleStore } from '../../stores/locale-store';
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
import { SafeAreaModal } from '../SafeAreaModal';
import { DrawerPanel } from '../DrawerPanel';

/** A calendar view listed at the top of the drawer. */
export interface DrawerViewOption {
Expand Down Expand Up @@ -172,7 +172,7 @@ export function CalendarSidebarDrawer({
<Animated.View
style={[styles.drawer, { transform: [{ translateX: slideX }] }]}
>
<SafeAreaView style={styles.drawerSafe} edges={['top', 'bottom', 'left']}>
<DrawerPanel style={styles.drawerSafe}>
<View style={styles.header}>
<Pressable
onPress={onClose}
Expand Down Expand Up @@ -269,7 +269,7 @@ export function CalendarSidebarDrawer({
</View>
)}
</ScrollView>
</SafeAreaView>
</DrawerPanel>
</Animated.View>
</SafeAreaModal>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/contacts/ContactsSidebarDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import {
View, Text, StyleSheet, Pressable, ScrollView, Animated, Dimensions, Easing, TextInput, Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import {
Expand All @@ -21,6 +20,7 @@ import { useColors } from '../../theme/colors';
import { useLocaleStore } from '../../stores/locale-store';
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
import { SafeAreaModal } from '../SafeAreaModal';
import { DrawerPanel } from '../DrawerPanel';

type Nav = NativeStackNavigationProp<RootStackParamList>;

Expand Down Expand Up @@ -197,7 +197,7 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
</Animated.View>

<Animated.View style={[styles.drawer, { transform: [{ translateX: slideX }] }]}>
<SafeAreaView style={styles.drawerSafe} edges={['top', 'bottom', 'left']}>
<DrawerPanel style={styles.drawerSafe}>
<View style={styles.header}>
<Pressable
onPress={onClose}
Expand Down Expand Up @@ -347,7 +347,7 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
<Text style={styles.hint}>{t('contacts.rename_tag_hint_mobile', 'Long-press a tag to rename it')}</Text>
)}
</ScrollView>
</SafeAreaView>
</DrawerPanel>
</Animated.View>
</SafeAreaModal>
);
Expand Down
Loading