From ccfa884f7c76f7ca0c31d7451115985e1e347d7c Mon Sep 17 00:00:00 2001 From: "satyajit.happy" Date: Wed, 26 Jun 2019 01:49:53 +0200 Subject: [PATCH] feat: improve keyboard handling during tab change Currently the keyboard is dismissed only when a swipe gesture starts. This commit changes the behaviour so that: - Keyboard is dismissed if the tab index changes - When a gesture begines, keyboard is dismissed, but if the index didn't change at end of the gesture, it's restored This is closer to iOS native behaviour and is based on previous work by [@skevy](https://github.com/skevy) in react-navgation: https://github.com/react-navigation/react-navigation/pull/3951 The old behaviour is left as the 'on-drag' option for backward compatibility. --- README.md | 3 ++- src/Pager.tsx | 69 +++++++++++++++++++++++++++++++++++++++---------- src/TabView.tsx | 2 +- src/types.tsx | 2 +- 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 43bde50c..8f166ffd 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,8 @@ Boolean indicating whether to remove invisible views (such as unfocused screens) String indicating whether the keyboard gets dismissed in response to a drag gesture. Possible values are: -- `'on-drag'` (default): the keyboard is dismissed when a drag begins. +- `'auto'` (default): the keyboard is dismissed when the index changes. +- `'on-drag'`: the keyboard is dismissed when a drag begins. - `'none'`: drags do not dismiss the keyboard. ##### `swipeEnabled` diff --git a/src/Pager.tsx b/src/Pager.tsx index 7b1aeb6a..26d36189 100644 --- a/src/Pager.tsx +++ b/src/Pager.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StyleSheet, Keyboard, I18nManager } from 'react-native'; +import { StyleSheet, TextInput, Keyboard, I18nManager } from 'react-native'; import { PanGestureHandler, State } from 'react-native-gesture-handler'; import Animated, { Easing } from 'react-native-reanimated'; import memoize from './memoize'; @@ -215,6 +215,12 @@ export default class Pager extends React.Component> { // Remember to set it before transition needs to occur private isSwipeGesture: Animated.Value = new Value(FALSE); + // Track the index value when a swipe gesture has ended + // This lets us know if a gesture end triggered a tab switch or not + private indexAtSwipeEnd: Animated.Value = new Value( + this.props.navigationState.index + ); + // Mappings to some prop values // We use them in animation calculations, so we need live animated nodes private routesLength = new Value(this.props.navigationState.routes.length); @@ -291,6 +297,10 @@ export default class Pager extends React.Component> { // It also needs to be reset right after componentDidUpdate fires private pendingIndexValue: number | undefined = undefined; + // Numeric id of the previously focused text input + // When a gesture didn't change the tab, we can restore the focused input with this + private previouslyFocusedTextInput: number | null = null; + // Listeners for the entered screen private enterListeners: Listener[] = []; @@ -301,7 +311,7 @@ export default class Pager extends React.Component> { }; private jumpTo = (key: string) => { - const { navigationState } = this.props; + const { navigationState, keyboardDismissMode, onIndexChange } = this.props; const index = navigationState.routes.findIndex(route => route.key === key); @@ -311,7 +321,13 @@ export default class Pager extends React.Component> { if (navigationState.index === index) { this.jumpToIndex(index); } else { - this.props.onIndexChange(index); + onIndexChange(index); + + // When the index changes, the focused input will no longer be in current tab + // So we should dismiss the keyboard + if (keyboardDismissMode === 'auto') { + Keyboard.dismiss(); + } } }; @@ -471,19 +487,43 @@ export default class Pager extends React.Component> { // Listen to updates for this value only when it changes // Without `onChange`, this will fire even if the value didn't change // We don't want to call the listeners if the value didn't change - call([this.isSwiping], ([value]: readonly Binary[]) => { - const { keyboardDismissMode, onSwipeStart, onSwipeEnd } = this.props; - - if (value === TRUE) { - onSwipeStart && onSwipeStart(); - - if (keyboardDismissMode === 'on-drag') { - Keyboard.dismiss(); + call( + [this.isSwiping, this.indexAtSwipeEnd, this.index], + ([isSwiping, indexAtSwipeEnd, currentIndex]: readonly number[]) => { + const { keyboardDismissMode, onSwipeStart, onSwipeEnd } = this.props; + + if (isSwiping === TRUE) { + onSwipeStart && onSwipeStart(); + + if (keyboardDismissMode === 'auto') { + const input = TextInput.State.currentlyFocusedField(); + + // When a gesture begins, blur the currently focused input + TextInput.State.blurTextInput(input); + + // Store the id of this input so we can refocus it if gesture was cancelled + this.previouslyFocusedTextInput = input; + } else if (keyboardDismissMode === 'on-drag') { + Keyboard.dismiss(); + } + } else { + onSwipeEnd && onSwipeEnd(); + + if (keyboardDismissMode === 'auto') { + if (indexAtSwipeEnd === currentIndex) { + // The index didn't change, we should restore the focus of text input + const input = this.previouslyFocusedTextInput; + + if (input) { + TextInput.State.focusTextInput(input); + } + } + + this.previouslyFocusedTextInput = null; + } } - } else { - onSwipeEnd && onSwipeEnd(); } - }) + ) ), onChange( this.nextIndex, @@ -518,6 +558,7 @@ export default class Pager extends React.Component> { ], [ set(this.isSwiping, FALSE), + set(this.indexAtSwipeEnd, this.index), this.transitionTo( cond( and( diff --git a/src/TabView.tsx b/src/TabView.tsx index 37bb1a11..e418c774 100644 --- a/src/TabView.tsx +++ b/src/TabView.tsx @@ -58,7 +58,7 @@ export default class TabView extends React.Component< ), renderLazyPlaceholder: () => null, - keyboardDismissMode: 'on-drag', + keyboardDismissMode: 'auto', swipeEnabled: true, lazy: false, lazyPreloadDistance: 0, diff --git a/src/types.tsx b/src/types.tsx index 567632a4..849386ba 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -37,7 +37,7 @@ export type EventEmitterProps = { }; export type PagerCommonProps = { - keyboardDismissMode: 'none' | 'on-drag'; + keyboardDismissMode: 'none' | 'on-drag' | 'auto'; swipeEnabled: boolean; swipeVelocityImpact?: number; onSwipeStart?: () => void;