mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
fix: move keyboardHandlingEnabled to screen options
This commit is contained in:
@@ -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?: {
|
||||
|
||||
105
packages/stack/src/utils/useKeyboardManager.tsx
Normal file
105
packages/stack/src/utils/useKeyboardManager.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user