Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove KeyboardAvoidingView compat layer #54101

Merged
Merged
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: 1 addition & 3 deletions src/components/HeaderWithBackButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed
import SearchButton from '@components/Search/SearchRouter/SearchButton';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import Tooltip from '@components/Tooltip';
import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
Expand Down Expand Up @@ -72,7 +71,6 @@ function HeaderWithBackButton({
const StyleUtils = useStyleUtils();
const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState();
const {translate} = useLocalize();
const {isKeyboardShown} = useKeyboardState();

// If the icon is present, the header bar should be taller and use different font.
const isCentralPaneSettings = !!icon;
Expand Down Expand Up @@ -155,7 +153,7 @@ function HeaderWithBackButton({
<Tooltip text={translate('common.back')}>
<PressableWithoutFeedback
onPress={() => {
if (isKeyboardShown) {
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
const topmostReportId = Navigation.getTopmostReportId();
Expand Down
138 changes: 10 additions & 128 deletions src/components/KeyboardAvoidingView/index.android.tsx
Original file line number Diff line number Diff line change
@@ -1,133 +1,15 @@
import React, {forwardRef, useCallback, useMemo, useState} from 'react';
import type {LayoutRectangle, View, ViewProps} from 'react-native';
import {useKeyboardContext, useKeyboardHandler} from 'react-native-keyboard-controller';
import Reanimated, {interpolate, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue} from 'react-native-reanimated';
import {useSafeAreaFrame} from 'react-native-safe-area-context';
import type {KeyboardAvoidingViewProps} from './types';

const useKeyboardAnimation = () => {
const {reanimated} = useKeyboardContext();

// calculate it only once on mount, to avoid `SharedValue` reads during a render
const [initialHeight] = useState(() => -reanimated.height.get());
const [initialProgress] = useState(() => reanimated.progress.get());

const heightWhenOpened = useSharedValue(initialHeight);
const height = useSharedValue(initialHeight);
const progress = useSharedValue(initialProgress);
const isClosed = useSharedValue(initialProgress === 0);

useKeyboardHandler(
{
onStart: (e) => {
'worklet';

progress.set(e.progress);
height.set(e.height);

if (e.height > 0) {
isClosed.set(false);
heightWhenOpened.set(e.height);
}
},
onEnd: (e) => {
'worklet';

isClosed.set(e.height === 0);
height.set(e.height);
progress.set(e.progress);
},
},
[],
);

return {height, progress, heightWhenOpened, isClosed};
};

const defaultLayout: LayoutRectangle = {
x: 0,
y: 0,
width: 0,
height: 0,
};

/**
* View that moves out of the way when the keyboard appears by automatically
* adjusting its height, position, or bottom padding.
*
* This `KeyboardAvoidingView` acts as a backward compatible layer for the previous Android behavior (prior to edge-to-edge mode).
* We can use `KeyboardAvoidingView` directly from the `react-native-keyboard-controller` package, but in this case animations are stuttering and it's better to handle as a separate task.
/*
* The KeyboardAvoidingView is only used on ios
*/
const KeyboardAvoidingView = forwardRef<View, React.PropsWithChildren<KeyboardAvoidingViewProps>>(
({behavior, children, contentContainerStyle, enabled = true, keyboardVerticalOffset = 0, style, onLayout: onLayoutProps, ...props}, ref) => {
const initialFrame = useSharedValue<LayoutRectangle | null>(null);
const frame = useDerivedValue(() => initialFrame.get() ?? defaultLayout);

const keyboard = useKeyboardAnimation();
const {height: screenHeight} = useSafeAreaFrame();

const relativeKeyboardHeight = useCallback(() => {
'worklet';

const keyboardY = screenHeight - keyboard.heightWhenOpened.get() - keyboardVerticalOffset;

return Math.max(frame.get().y + frame.get().height - keyboardY, 0);
}, [screenHeight, keyboard.heightWhenOpened, keyboardVerticalOffset, frame]);

const onLayoutWorklet = useCallback(
(layout: LayoutRectangle) => {
'worklet';

if (keyboard.isClosed.get() || initialFrame.get() === null) {
initialFrame.set(layout);
}
},
[initialFrame, keyboard.isClosed],
);
const onLayout = useCallback<NonNullable<ViewProps['onLayout']>>(
(e) => {
runOnUI(onLayoutWorklet)(e.nativeEvent.layout);
onLayoutProps?.(e);
},
[onLayoutProps, onLayoutWorklet],
);

const animatedStyle = useAnimatedStyle(() => {
const bottom = interpolate(keyboard.progress.get(), [0, 1], [0, relativeKeyboardHeight()]);
const bottomHeight = enabled ? bottom : 0;

switch (behavior) {
case 'height':
if (!keyboard.isClosed.get()) {
return {
height: frame.get().height - bottomHeight,
flex: 0,
};
}

return {};

case 'padding':
return {paddingBottom: bottomHeight};
import React from 'react';
import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native-keyboard-controller';
import type {KeyboardAvoidingViewProps} from './types';

default:
return {};
}
}, [behavior, enabled, relativeKeyboardHeight]);
const combinedStyles = useMemo(() => [style, animatedStyle], [style, animatedStyle]);
function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <KeyboardAvoidingViewComponent {...props} />;
}

return (
<Reanimated.View
ref={ref}
style={combinedStyles}
onLayout={onLayout}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{children}
</Reanimated.View>
);
},
);
KeyboardAvoidingView.displayName = 'KeyboardAvoidingView';

export default KeyboardAvoidingView;
12 changes: 2 additions & 10 deletions src/components/ScreenWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {PickerAvoidingView} from 'react-native-picker-select';
import type {EdgeInsets} from 'react-native-safe-area-context';
import useEnvironment from '@hooks/useEnvironment';
import useInitialDimensions from '@hooks/useInitialWindowDimensions';
import useKeyboardState from '@hooks/useKeyboardState';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
Expand Down Expand Up @@ -158,18 +157,11 @@ function ScreenWrapper(
const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
const {initialHeight} = useInitialDimensions();
const styles = useThemeStyles();
const keyboardState = useKeyboardState();
const {isDevelopment} = useEnvironment();
const {isOffline} = useNetwork();
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined;
const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined;
const isKeyboardShown = keyboardState?.isKeyboardShown ?? false;

const isKeyboardShownRef = useRef<boolean>(false);

// eslint-disable-next-line react-compiler/react-compiler
isKeyboardShownRef.current = keyboardState?.isKeyboardShown ?? false;

const route = useRoute();
const shouldReturnToOldDot = useMemo(() => {
Expand All @@ -191,7 +183,7 @@ function ScreenWrapper(
PanResponder.create({
onMoveShouldSetPanResponderCapture: (_e, gestureState) => {
const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && isKeyboardShown && Browser.isMobile();
const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && Browser.isMobile();

return isHorizontalSwipe && shouldDismissKeyboard;
},
Expand Down Expand Up @@ -221,7 +213,7 @@ function ScreenWrapper(
// described here https://reactnavigation.org/docs/preventing-going-back/#limitations
const beforeRemoveSubscription = shouldDismissKeyboardBeforeClose
? navigation.addListener('beforeRemove', () => {
if (!isKeyboardShownRef.current) {
if (!Keyboard.isVisible()) {
return;
}
Keyboard.dismiss();
Expand Down
27 changes: 2 additions & 25 deletions src/components/withKeyboardState.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {ComponentType, ForwardedRef, ReactElement, RefAttributes} from 'react';
import React, {createContext, forwardRef, useEffect, useMemo, useState} from 'react';
import type {ReactElement} from 'react';
import React, {createContext, useEffect, useMemo, useState} from 'react';
import {Keyboard} from 'react-native';
import getComponentDisplayName from '@libs/getComponentDisplayName';
import type ChildrenProps from '@src/types/utils/ChildrenProps';

type KeyboardStateContextValue = {
Expand Down Expand Up @@ -44,27 +43,5 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null {
return <KeyboardStateContext.Provider value={contextValue}>{children}</KeyboardStateContext.Provider>;
}

export default function withKeyboardState<TProps extends KeyboardStateContextValue, TRef>(
WrappedComponent: ComponentType<TProps & RefAttributes<TRef>>,
): (props: Omit<TProps, keyof KeyboardStateContextValue> & React.RefAttributes<TRef>) => ReactElement | null {
function WithKeyboardState(props: Omit<TProps, keyof KeyboardStateContextValue>, ref: ForwardedRef<TRef>) {
return (
<KeyboardStateContext.Consumer>
{(keyboardStateProps) => (
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...keyboardStateProps}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(props as TProps)}
ref={ref}
/>
)}
</KeyboardStateContext.Consumer>
);
}
WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent)})`;
return forwardRef(WithKeyboardState);
}

export type {KeyboardStateContextValue};
export {KeyboardStateProvider, KeyboardStateContext};
Loading