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
7 changes: 6 additions & 1 deletion src/components/FloatingActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { MenuAction, MenuView } from '@react-native-menu/menu';
import { ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { StyleSheet, View } from 'react-native';
import { Keyboard, StyleSheet, View } from 'react-native';
import { Icon } from 'react-native-elements';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

Expand Down Expand Up @@ -51,6 +51,11 @@ const FloatingActionButton: React.FunctionComponent<Props> = ({ navigation }) =>
}]}>
<MenuView
style={StyleSheet.absoluteFill}
// A text input elsewhere (e.g. the edit-player name field) can leave
// a dangling first responder that iOS tries to restore when this
// native menu presents, popping the keyboard up alongside it.
// Dismiss the keyboard as the menu opens so that can't happen.
onOpenMenu={() => Keyboard.dismiss()}
onPressAction={async ({ nativeEvent }) => {
const playerNumber = parseInt(nativeEvent.event);
addGameHandler(playerNumber);
Expand Down
40 changes: 40 additions & 0 deletions src/screens/EditPlayerScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';

import { configureStore } from '@reduxjs/toolkit';
import { act, fireEvent, render, waitFor } from '@testing-library/react-native';
import { Keyboard } from 'react-native';
import { Provider } from 'react-redux';

import gamesReducer from '../../redux/GamesSlice';
Expand Down Expand Up @@ -463,6 +464,45 @@ describe('EditPlayerScreen', () => {
expect(input).toBeTruthy();
});

it('dismisses the keyboard when the screen is removed', () => {
// Regression: the clear button focuses the input programmatically. If the
// screen unmounts while it is still the first responder, iOS re-shows the
// keyboard the next time a native menu (the new-game player-count menu)
// presents. Leaving the screen must dismiss the keyboard.
const dismissSpy = jest.spyOn(Keyboard, 'dismiss').mockImplementation(() => { });

let beforeRemoveCallback: (() => void) | undefined;
mockNavigation.addListener.mockImplementation((event: string, cb: () => void) => {
if (event === 'beforeRemove') {
beforeRemoveCallback = cb;
}
return jest.fn();
});

const mockRoute = {
params: {
index: 0,
playerId: 'player-1',
},
};

render(
<Provider store={mockStore}>
<EditPlayerScreen navigation={mockNavigation} route={mockRoute as any} />
</Provider>
);

expect(beforeRemoveCallback).toBeDefined();

act(() => {
beforeRemoveCallback?.();
});

expect(dismissSpy).toHaveBeenCalled();

dismissSpy.mockRestore();
});

it('should limit input to 15 characters', () => {
const store = createMockStore({
settings: {
Expand Down
19 changes: 18 additions & 1 deletion src/screens/EditPlayerScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';

import { ParamListBase, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import {
Keyboard,
NativeSyntheticEvent,
ScrollView,
StyleSheet,
Expand Down Expand Up @@ -45,6 +46,10 @@ const EditPlayerScreen: React.FC<EditPlayerScreenProps> = ({
navigation.setOptions({
headerLeft: ({ tintColor }) => (
<HeaderButton accessibilityLabel='EditPlayerBack' onPress={async () => {
// Resign the first responder before the back transition starts,
// while the input is still mounted, so iOS doesn't keep it around
// and re-show the keyboard later (see beforeRemove handler below).
Keyboard.dismiss();
navigation.goBack();
await logEvent('edit_player_back');
}}>
Expand All @@ -53,6 +58,18 @@ const EditPlayerScreen: React.FC<EditPlayerScreenProps> = ({
),
});
}, [navigation, theme.tint]);

// The clear button focuses the input programmatically. If the screen is
// removed while that input is still the first responder, iOS keeps a
// dangling reference and re-shows the keyboard the next time a native view
// (e.g. the new-game player-count menu) presents. Dismiss it on the way out.
useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', () => {
Keyboard.dismiss();
});
return unsubscribe;
}, [navigation]);

const dispatch = useAppDispatch();
const currentGame = useAppSelector(selectCurrentGame);
const { index, playerId } = route.params;
Expand Down
Loading