fix: move keyboardHandlingEnabled to screen options

This commit is contained in:
Satyajit Sahoo
2021-05-16 03:58:28 +02:00
parent dc4ffc0171
commit 82900cceff
6 changed files with 153 additions and 167 deletions

View File

@@ -292,18 +292,19 @@ export type StackNavigationOptions = StackHeaderOptions &
* Defaults to `false` for the last screen for modals, otherwise `true`.
*/
detachPreviousScreen?: boolean;
/**
* If `false`, the keyboard will NOT automatically dismiss when navigating to a new screen from this screen.
* Defaults to `true`.
*/
keyboardHandlingEnabled?: boolean;
};
export type StackNavigationConfig = {
/**
* If `false`, the keyboard will NOT automatically dismiss when navigating to a new screen.
* Defaults to `true`.
*/
keyboardHandlingEnabled?: boolean;
/**
* Whether inactive screens should be detached from the view hierarchy to save memory.
* Make sure to call `enableScreens` from `react-native-screens` to make it work.
* Defaults to `true` on Android, depends on the version of `react-native-screens` on iOS.
* This will have no effect if you disable `react-native-screens`.
*
* Defaults to `true`.
*/
detachInactiveScreens?: boolean;
};
@@ -335,7 +336,7 @@ export type StackCardInterpolationProps = {
progress: Animated.AnimatedInterpolation;
};
/**
* Values for the current screen the screen after this one in the stack.
* Values for the screen after this one in the stack.
* This can be `undefined` in case the screen animating is the last one.
*/
next?: {
@@ -414,7 +415,7 @@ export type StackHeaderInterpolationProps = {
progress: Animated.AnimatedInterpolation;
};
/**
* Values for the current screen the screen after this one in the stack.
* Values for the screen after this one in the stack.
* This can be `undefined` in case the screen animating is the last one.
*/
next?: {

View File

@@ -0,0 +1,105 @@
import * as React from 'react';
import { TextInput, Keyboard, HostComponent } from 'react-native';
type InputRef = React.ElementRef<HostComponent<unknown>> | undefined;
export default function useKeyboardManager(isEnabled: () => boolean) {
// Numeric id of the previously focused text input
// When a gesture didn't change the tab, we can restore the focused input with this
const previouslyFocusedTextInputRef = React.useRef<InputRef>(undefined);
const startTimestampRef = React.useRef<number>(0);
const keyboardTimeoutRef = React.useRef<any>();
const clearKeyboardTimeout = React.useCallback(() => {
if (keyboardTimeoutRef.current !== undefined) {
clearTimeout(keyboardTimeoutRef.current);
keyboardTimeoutRef.current = undefined;
}
}, []);
const onPageChangeStart = React.useCallback(() => {
if (!isEnabled()) {
return;
}
clearKeyboardTimeout();
const input: InputRef = TextInput.State.currentlyFocusedInput();
// When a page change begins, blur the currently focused input
input?.blur();
// Store the id of this input so we can refocus it if change was cancelled
previouslyFocusedTextInputRef.current = input;
// Store timestamp for touch start
startTimestampRef.current = Date.now();
}, [clearKeyboardTimeout, isEnabled]);
const onPageChangeConfirm = React.useCallback(
(force: boolean) => {
if (!isEnabled()) {
return;
}
clearKeyboardTimeout();
if (force) {
// Always dismiss input, even if we don't have a ref to it
// We might not have the ref if onPageChangeStart was never called
// This can happen if page change was not from a gesture
Keyboard.dismiss();
} else {
const input = previouslyFocusedTextInputRef.current;
// Dismiss the keyboard only if an input was a focused before
// This makes sure we don't dismiss input on going back and focusing an input
input?.blur();
}
// Cleanup the ID on successful page change
previouslyFocusedTextInputRef.current = undefined;
},
[clearKeyboardTimeout, isEnabled]
);
const onPageChangeCancel = React.useCallback(() => {
if (!isEnabled()) {
return;
}
clearKeyboardTimeout();
// The page didn't change, we should restore the focus of text input
const input = previouslyFocusedTextInputRef.current;
if (input) {
// If the interaction was super short we should make sure keyboard won't hide again.
// Too fast input refocus will result only in keyboard flashing on screen and hiding right away.
// During first ~100ms keyboard will be dismissed no matter what,
// so we have to make sure it won't interrupt input refocus logic.
// That's why when the interaction is shorter than 100ms we add delay so it won't hide once again.
// Subtracting timestamps makes us sure the delay is executed only when needed.
if (Date.now() - startTimestampRef.current < 100) {
keyboardTimeoutRef.current = setTimeout(() => {
input?.focus();
previouslyFocusedTextInputRef.current = undefined;
}, 100);
} else {
input?.focus();
previouslyFocusedTextInputRef.current = undefined;
}
}
}, [clearKeyboardTimeout, isEnabled]);
React.useEffect(() => {
return () => clearKeyboardTimeout();
}, [clearKeyboardTimeout]);
return {
onPageChangeStart,
onPageChangeConfirm,
onPageChangeCancel,
};
}

View File

@@ -1,113 +0,0 @@
import * as React from 'react';
import { TextInput, Keyboard, HostComponent } from 'react-native';
type Props = {
enabled: boolean;
children: (props: {
onPageChangeStart: () => void;
onPageChangeConfirm: (force: boolean) => void;
onPageChangeCancel: () => void;
}) => React.ReactNode;
};
type InputRef = React.ElementRef<HostComponent<unknown>> | undefined;
export default class KeyboardManager extends React.Component<Props> {
componentWillUnmount() {
this.clearKeyboardTimeout();
}
// 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: InputRef = undefined;
private startTimestamp: number = 0;
private keyboardTimeout: any;
private clearKeyboardTimeout = () => {
if (this.keyboardTimeout !== undefined) {
clearTimeout(this.keyboardTimeout);
this.keyboardTimeout = undefined;
}
};
private handlePageChangeStart = () => {
if (!this.props.enabled) {
return;
}
this.clearKeyboardTimeout();
const input: InputRef = TextInput.State.currentlyFocusedInput();
// When a page change begins, blur the currently focused input
input?.blur();
// Store the id of this input so we can refocus it if change was cancelled
this.previouslyFocusedTextInput = input;
// Store timestamp for touch start
this.startTimestamp = Date.now();
};
private handlePageChangeConfirm = (force: boolean) => {
if (!this.props.enabled) {
return;
}
this.clearKeyboardTimeout();
if (force) {
// Always dismiss input, even if we don't have a ref to it
// We might not have the ref if onPageChangeStart was never called
// This can happen if page change was not from a gesture
Keyboard.dismiss();
} else {
const input = this.previouslyFocusedTextInput;
// Dismiss the keyboard only if an input was a focused before
// This makes sure we don't dismiss input on going back and focusing an input
input?.blur();
}
// Cleanup the ID on successful page change
this.previouslyFocusedTextInput = undefined;
};
private handlePageChangeCancel = () => {
if (!this.props.enabled) {
return;
}
this.clearKeyboardTimeout();
// The page didn't change, we should restore the focus of text input
const input = this.previouslyFocusedTextInput;
if (input) {
// If the interaction was super short we should make sure keyboard won't hide again.
// Too fast input refocus will result only in keyboard flashing on screen and hiding right away.
// During first ~100ms keyboard will be dismissed no matter what,
// so we have to make sure it won't interrupt input refocus logic.
// That's why when the interaction is shorter than 100ms we add delay so it won't hide once again.
// Subtracting timestamps makes us sure the delay is executed only when needed.
if (Date.now() - this.startTimestamp < 100) {
this.keyboardTimeout = setTimeout(() => {
input?.focus();
this.previouslyFocusedTextInput = undefined;
}, 100);
} else {
input?.focus();
this.previouslyFocusedTextInput = undefined;
}
}
};
render() {
return this.props.children({
onPageChangeStart: this.handlePageChangeStart,
onPageChangeConfirm: this.handlePageChangeConfirm,
onPageChangeCancel: this.handlePageChangeCancel,
});
}
}

View File

@@ -11,6 +11,7 @@ import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import Card from './Card';
import { forModalPresentationIOS } from '../../TransitionConfigs/CardStyleInterpolators';
import ModalPresentationContext from '../../utils/ModalPresentationContext';
import useKeyboardManager from '../../utils/useKeyboardManager';
import type { Layout, Scene } from '../../types';
type Props = {
@@ -37,9 +38,6 @@ type Props = {
closing: boolean
) => void;
onTransitionEnd?: (props: { route: Route<string> }, closing: boolean) => void;
onPageChangeStart?: () => void;
onPageChangeConfirm?: (force: boolean) => void;
onPageChangeCancel?: () => void;
onGestureStart?: (props: { route: Route<string> }) => void;
onGestureEnd?: (props: { route: Route<string> }) => void;
onGestureCancel?: (props: { route: Route<string> }) => void;
@@ -70,9 +68,6 @@ function CardContainer({
layout,
onCloseRoute,
onOpenRoute,
onPageChangeCancel,
onPageChangeConfirm,
onPageChangeStart,
onGestureCancel,
onGestureEnd,
onGestureStart,
@@ -88,6 +83,20 @@ function CardContainer({
}: Props) {
const parentHeaderHeight = React.useContext(HeaderHeightContext);
const {
onPageChangeStart,
onPageChangeCancel,
onPageChangeConfirm,
} = useKeyboardManager(
React.useCallback(() => {
const { options, navigation } = scene.descriptor;
return (
navigation.isFocused() && options.keyboardHandlingEnabled !== false
);
}, [scene.descriptor])
);
const handleOpen = () => {
const { route } = scene.descriptor;

View File

@@ -63,9 +63,6 @@ type Props = {
closing: boolean
) => void;
onTransitionEnd: (props: { route: Route<string> }, closing: boolean) => void;
onPageChangeStart?: () => void;
onPageChangeConfirm?: (force: boolean) => void;
onPageChangeCancel?: () => void;
onGestureStart?: (props: { route: Route<string> }) => void;
onGestureEnd?: (props: { route: Route<string> }) => void;
onGestureCancel?: (props: { route: Route<string> }) => void;
@@ -430,9 +427,6 @@ export default class CardStack extends React.Component<Props, State> {
isParentHeaderShown,
onTransitionStart,
onTransitionEnd,
onPageChangeStart,
onPageChangeConfirm,
onPageChangeCancel,
onGestureStart,
onGestureEnd,
onGestureCancel,
@@ -603,9 +597,6 @@ export default class CardStack extends React.Component<Props, State> {
safeAreaInsetRight={safeAreaInsetRight}
safeAreaInsetBottom={safeAreaInsetBottom}
safeAreaInsetLeft={safeAreaInsetLeft}
onPageChangeStart={onPageChangeStart}
onPageChangeConfirm={onPageChangeConfirm}
onPageChangeCancel={onPageChangeCancel}
onGestureStart={onGestureStart}
onGestureCancel={onGestureCancel}
onGestureEnd={onGestureEnd}

View File

@@ -17,7 +17,6 @@ import {
import { GestureHandlerRootView } from '../GestureHandler';
import CardStack from './CardStack';
import KeyboardManager from '../KeyboardManager';
import HeaderContainer, {
Props as HeaderContainerProps,
} from '../Header/HeaderContainer';
@@ -417,7 +416,6 @@ export default class StackView extends React.Component<Props, State> {
render() {
const {
state,
keyboardHandlingEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
descriptors: _,
...rest
@@ -435,35 +433,30 @@ export default class StackView extends React.Component<Props, State> {
<SafeAreaProviderCompat>
<SafeAreaInsetsContext.Consumer>
{(insets) => (
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
{(props) => (
<HeaderShownContext.Consumer>
{(isParentHeaderShown) => (
<CardStack
insets={insets as EdgeInsets}
isParentHeaderShown={isParentHeaderShown}
getPreviousRoute={this.getPreviousRoute}
routes={routes}
openingRouteKeys={openingRouteKeys}
closingRouteKeys={closingRouteKeys}
onOpenRoute={this.handleOpenRoute}
onCloseRoute={this.handleCloseRoute}
onTransitionStart={this.handleTransitionStart}
onTransitionEnd={this.handleTransitionEnd}
renderHeader={this.renderHeader}
renderScene={this.renderScene}
state={state}
descriptors={descriptors}
onGestureStart={this.handleGestureStart}
onGestureEnd={this.handleGestureEnd}
onGestureCancel={this.handleGestureCancel}
{...rest}
{...props}
/>
)}
</HeaderShownContext.Consumer>
<HeaderShownContext.Consumer>
{(isParentHeaderShown) => (
<CardStack
insets={insets as EdgeInsets}
isParentHeaderShown={isParentHeaderShown}
getPreviousRoute={this.getPreviousRoute}
routes={routes}
openingRouteKeys={openingRouteKeys}
closingRouteKeys={closingRouteKeys}
onOpenRoute={this.handleOpenRoute}
onCloseRoute={this.handleCloseRoute}
onTransitionStart={this.handleTransitionStart}
onTransitionEnd={this.handleTransitionEnd}
renderHeader={this.renderHeader}
renderScene={this.renderScene}
state={state}
descriptors={descriptors}
onGestureStart={this.handleGestureStart}
onGestureEnd={this.handleGestureEnd}
onGestureCancel={this.handleGestureCancel}
{...rest}
/>
)}
</KeyboardManager>
</HeaderShownContext.Consumer>
)}
</SafeAreaInsetsContext.Consumer>
</SafeAreaProviderCompat>