Skip to content

Commit 9b59683

Browse files
authored
Merge pull request #88122 from software-mansion-labs/fix/landscape-transition-keyboard-focus
2 parents eb88081 + 787000d commit 9b59683

5 files changed

Lines changed: 50 additions & 1 deletion

File tree

src/components/RNMarkdownTextInput.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {ForwardedRef} from 'react';
44
import React, {useCallback, useEffect, useRef} from 'react';
55
import {View} from 'react-native';
66
import Animated, {useSharedValue} from 'react-native-reanimated';
7+
import useLandscapeOnBlurProxy from '@hooks/useLandscapeOnBlurProxy';
78
import useShortMentionsList from '@hooks/useShortMentionsList';
89
import useTheme from '@hooks/useTheme';
910
import useThemeStyles from '@hooks/useThemeStyles';
@@ -36,6 +37,8 @@ function RNMarkdownTextInputWithRef({maxLength, parser, ref, forwardedFSClass =
3637
// Expose the ref to the parent component
3738
React.useImperativeHandle<AnimatedMarkdownTextInputRef | null, AnimatedMarkdownTextInputRef | null>(ref, () => inputRef.current);
3839

40+
const handleBlur = useLandscapeOnBlurProxy(inputRef, props.onBlur);
41+
3942
// Check if the cursor is at the end of the text
4043
const isCursorAtEnd = props.selection && props.value && props.selection.start === props.value.length;
4144

@@ -96,6 +99,7 @@ function RNMarkdownTextInputWithRef({maxLength, parser, ref, forwardedFSClass =
9699
* If maxLength is not set, we should set it to CONST.MAX_COMMENT_LENGTH + 1, to avoid parsing markdown for large text
97100
*/
98101
maxLength={maxLength ?? CONST.MAX_COMMENT_LENGTH + 1}
102+
onBlur={handleBlur}
99103
/>
100104
</View>
101105
);

src/components/RNTextInput.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type {ForwardedRef} from 'react';
2-
import React from 'react';
2+
import React, {useRef} from 'react';
33
import type {TextInputProps} from 'react-native';
44
import {TextInput} from 'react-native';
55
import Animated from 'react-native-reanimated';
6+
import useLandscapeOnBlurProxy from '@hooks/useLandscapeOnBlurProxy';
67
import useTheme from '@hooks/useTheme';
78
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
89
import CONST from '@src/CONST';
@@ -19,13 +20,16 @@ type RNTextInputWithRefProps = TextInputProps &
1920

2021
function RNTextInputWithRef({ref, forwardedFSClass = CONST.FULLSTORY.CLASS.UNMASK, ...props}: RNTextInputWithRefProps) {
2122
const theme = useTheme();
23+
const inputRef = useRef<AnimatedTextInputRef | null>(null);
24+
const handleBlur = useLandscapeOnBlurProxy(inputRef, props.onBlur);
2225

2326
return (
2427
<AnimatedTextInput
2528
allowFontScaling={false}
2629
textBreakStrategy="simple"
2730
keyboardAppearance={theme.colorScheme}
2831
ref={(refHandle: AnimatedTextInputRef) => {
32+
inputRef.current = refHandle;
2933
if (typeof ref !== 'function') {
3034
return;
3135
}
@@ -35,6 +39,7 @@ function RNTextInputWithRef({ref, forwardedFSClass = CONST.FULLSTORY.CLASS.UNMAS
3539
fsClass={forwardedFSClass}
3640
// eslint-disable-next-line
3741
{...props}
42+
onBlur={handleBlur}
3843
/>
3944
);
4045
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode';
2+
import usePrevious from '@hooks/usePrevious';
3+
import type {UseLandscapeOnBlurProxy} from './types';
4+
5+
// During a portrait → landscape rotation the input briefly ends up behind the keyboard
6+
// while KeyboardAvoidingView catches up, and native blurs it as a result. When that blur
7+
// fires we re-focus the input after a short delay — long enough for KAV to reposition so
8+
// the input is on-screen again, otherwise the re-focus gets clobbered by the same issue.
9+
const ROTATION_REFOCUS_DELAY_MS = 100;
10+
11+
const useLandscapeOnBlurProxy: UseLandscapeOnBlurProxy = (inputRef, onBlur) => {
12+
const isInLandscapeMode = useIsInLandscapeMode();
13+
const prevIsInLandscapeMode = usePrevious(isInLandscapeMode);
14+
15+
return (e) => {
16+
if (prevIsInLandscapeMode !== isInLandscapeMode && isInLandscapeMode) {
17+
setTimeout(() => inputRef.current?.focus?.(), ROTATION_REFOCUS_DELAY_MS);
18+
}
19+
onBlur?.(e);
20+
};
21+
};
22+
23+
export default useLandscapeOnBlurProxy;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type {UseLandscapeOnBlurProxy} from './types';
2+
3+
// The rotation-refocus workaround is only needed on Android — iOS and web don't lose focus
4+
// when the orientation flips, so we pass the caller's onBlur through unchanged.
5+
const useLandscapeOnBlurProxy: UseLandscapeOnBlurProxy = (_inputRef, onBlur) => onBlur;
6+
7+
export default useLandscapeOnBlurProxy;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type {RefObject} from 'react';
2+
import type {FocusEvent} from 'react-native';
3+
4+
type FocusableRef = RefObject<{focus?: () => void} | null>;
5+
6+
type OnBlurHandler = (e: FocusEvent) => void;
7+
8+
type UseLandscapeOnBlurProxy = (inputRef: FocusableRef, onBlur?: OnBlurHandler) => OnBlurHandler | undefined;
9+
10+
export type {FocusableRef, OnBlurHandler, UseLandscapeOnBlurProxy};

0 commit comments

Comments
 (0)