Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/components/Composer/implementation/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {ComposerProps, ComposerRef} from '@components/Composer/types';
import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput';
import RNMarkdownTextInput from '@components/RNMarkdownTextInput';

import useBlurOnKeyboardHide from '@hooks/useBlurOnKeyboardHide';
import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode';
import useMarkdownStyle from '@hooks/useMarkdownStyle';
import useStyleUtils from '@hooks/useStyleUtils';
Expand Down Expand Up @@ -52,6 +53,9 @@ function Composer({
const StyleUtils = useStyleUtils();
const isInLandscapeMode = useIsInLandscapeMode();

// Android keeps the input focused when the keyboard is dismissed with the back button/gesture.
useBlurOnKeyboardHide(textInputRef);

useEffect(() => {
if (!textInputRef.current?.setSelection || !selection || isComposerFullSize) {
return;
Expand Down
24 changes: 24 additions & 0 deletions src/hooks/useBlurOnKeyboardHide/index.android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {useEffect} from 'react';
import {KeyboardEvents} from 'react-native-keyboard-controller';

import type UseBlurOnKeyboardHide from './type';

/**
* Android hides the soft keyboard on a back press/gesture without clearing focus from the native EditText,
* so JS never receives onBlur and the input stays focused. Blur it explicitly once the keyboard is gone.
*/
const useBlurOnKeyboardHide: UseBlurOnKeyboardHide = (ref) => {
useEffect(() => {
const subscription = KeyboardEvents.addListener('keyboardDidHide', (event) => {
// keyboardDidHide can fire mid-gesture with the keyboard still partly on screen. Only the zero-height
// event means it is actually gone, so blurring on the earlier ones would break a cancelled swipe.
if (event.height !== 0) {
return;
}
ref.current?.blur();
});
return () => subscription.remove();
}, [ref]);
};

export default useBlurOnKeyboardHide;
6 changes: 6 additions & 0 deletions src/hooks/useBlurOnKeyboardHide/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type UseBlurOnKeyboardHide from './type';

/** Every platform other than android blurs the input on its own when the keyboard is dismissed. */
const useBlurOnKeyboardHide: UseBlurOnKeyboardHide = () => {};

export default useBlurOnKeyboardHide;
5 changes: 5 additions & 0 deletions src/hooks/useBlurOnKeyboardHide/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type {RefObject} from 'react';

type UseBlurOnKeyboardHide = (ref: RefObject<{blur: () => void} | null>) => void;

export default UseBlurOnKeyboardHide;
44 changes: 44 additions & 0 deletions tests/unit/hooks/useBlurOnKeyboardHide.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {renderHook} from '@testing-library/react-native';

import useBlurOnKeyboardHide from '@hooks/useBlurOnKeyboardHide/index.android';

import type {KeyboardEventData} from 'react-native-keyboard-controller';

const mockKeyboardDidHideListeners: Array<(e: KeyboardEventData) => void> = [];
const mockRemove = jest.fn();

jest.mock('react-native-keyboard-controller', () => ({
KeyboardEvents: {
addListener: jest.fn((event: string, handler: (e: KeyboardEventData) => void) => {
if (event === 'keyboardDidHide') {
mockKeyboardDidHideListeners.push(handler);
}
return {remove: mockRemove};
}),
},
}));

function emitKeyboardDidHide(height: number) {
const event: KeyboardEventData = {height, duration: 0, timestamp: 0, target: -1, type: 'default', appearance: 'light'};
for (const handler of mockKeyboardDidHideListeners) {
handler(event);
}
}

describe('useBlurOnKeyboardHide', () => {
beforeEach(() => {
mockKeyboardDidHideListeners.length = 0;
jest.clearAllMocks();
});

it('blurs the ref only when keyboardDidHide reports zero height', () => {
const blur = jest.fn();
renderHook(() => useBlurOnKeyboardHide({current: {blur}}));

emitKeyboardDidHide(100);
expect(blur).not.toHaveBeenCalled();

emitKeyboardDidHide(0);
expect(blur).toHaveBeenCalledTimes(1);
});
});
Loading