mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: keep the event editor's bottom fields above the keyboard #81
This commit is contained in:
@@ -8,8 +8,9 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Platform,
|
Platform,
|
||||||
Alert as NativeAlert,
|
Alert as NativeAlert,
|
||||||
|
type LayoutChangeEvent,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
@@ -72,6 +73,8 @@ import { useContactNameResolver } from '../../lib/contact-name-resolver';
|
|||||||
import { RecurrenceEditor } from './RecurrenceEditor';
|
import { RecurrenceEditor } from './RecurrenceEditor';
|
||||||
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
import { MODAL_ORIENTATIONS } from '../../lib/modal-orientations';
|
||||||
import { SafeAreaModal } from '../SafeAreaModal';
|
import { SafeAreaModal } from '../SafeAreaModal';
|
||||||
|
import { useKeyboardObstruction } from '../../lib/use-keyboard-obstruction';
|
||||||
|
import { editorKeyboardInset } from '../../lib/calendar-editor-keyboard';
|
||||||
import ToggleSwitch from '../ToggleSwitch';
|
import ToggleSwitch from '../ToggleSwitch';
|
||||||
|
|
||||||
type RecurrenceOption = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
|
type RecurrenceOption = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom';
|
||||||
@@ -185,6 +188,25 @@ export function EventModal({
|
|||||||
const isEdit = !!event;
|
const isEdit = !!event;
|
||||||
const resolveContactName = useContactNameResolver();
|
const resolveContactName = useContactNameResolver();
|
||||||
|
|
||||||
|
// The keyboard covered the bottom fields (location, description; #81).
|
||||||
|
// iOS: the scroll view insets itself and scrolls the focused field into
|
||||||
|
// view (automaticallyAdjustKeyboardInsets). Android: the edge-to-edge
|
||||||
|
// dialog does not resize for the keyboard, so the scroll view ends above
|
||||||
|
// it, and Android's ScrollView brings the focused field back into view
|
||||||
|
// when it shrinks.
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const keyboard = useKeyboardObstruction(insets.bottom);
|
||||||
|
const fullHeight = React.useRef(0);
|
||||||
|
const [containerHeight, setContainerHeight] = React.useState(0);
|
||||||
|
const onContainerLayout = (e: LayoutChangeEvent) => {
|
||||||
|
const { height } = e.nativeEvent.layout;
|
||||||
|
if (keyboard === 0) fullHeight.current = height;
|
||||||
|
setContainerHeight(height);
|
||||||
|
};
|
||||||
|
const keyboardInset = Platform.OS === 'android'
|
||||||
|
? editorKeyboardInset(keyboard, insets.bottom, fullHeight.current - containerHeight)
|
||||||
|
: 0;
|
||||||
|
|
||||||
const [title, setTitle] = React.useState('');
|
const [title, setTitle] = React.useState('');
|
||||||
const [description, setDescription] = React.useState('');
|
const [description, setDescription] = React.useState('');
|
||||||
const [allDay, setAllDay] = React.useState(false);
|
const [allDay, setAllDay] = React.useState(false);
|
||||||
@@ -441,7 +463,7 @@ export function EventModal({
|
|||||||
presentationStyle="fullScreen"
|
presentationStyle="fullScreen"
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
>
|
>
|
||||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']} onLayout={onContainerLayout}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
@@ -467,9 +489,10 @@ export function EventModal({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.body}
|
style={[styles.body, keyboardInset > 0 && { marginBottom: keyboardInset }]}
|
||||||
contentContainerStyle={styles.bodyContent}
|
contentContainerStyle={styles.bodyContent}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
|
automaticallyAdjustKeyboardInsets
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={title}
|
value={title}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { editorKeyboardInset } from '../calendar-editor-keyboard';
|
||||||
|
|
||||||
|
describe('editorKeyboardInset (#81)', () => {
|
||||||
|
it('ends the scroll area above the keyboard, past the safe-area inset already kept clear', () => {
|
||||||
|
expect(editorKeyboardInset(320, 24, 0)).toBe(296);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shortens a window that resized for the keyboard by nothing more', () => {
|
||||||
|
expect(editorKeyboardInset(320, 24, 296)).toBe(0);
|
||||||
|
expect(editorKeyboardInset(320, 24, 400)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the full height without a keyboard', () => {
|
||||||
|
expect(editorKeyboardInset(0, 24, 0)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* How far above its normal bottom the event editor's scroll area has to end
|
||||||
|
* so the keyboard covers none of it (#81). `obstruction` is how much of the
|
||||||
|
* screen bottom the keyboard covers (use-keyboard-obstruction),
|
||||||
|
* `bottomInset` the safe-area inset the editor already keeps clear, and
|
||||||
|
* `resizedBy` how much the window itself shrank for the keyboard: an
|
||||||
|
* edge-to-edge Android dialog does not resize at all, a window that does
|
||||||
|
* must not be shortened twice.
|
||||||
|
*/
|
||||||
|
export function editorKeyboardInset(obstruction: number, bottomInset: number, resizedBy: number): number {
|
||||||
|
return Math.max(0, Math.round(obstruction - bottomInset - Math.max(0, resizedBy)));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user