Skip to content

Commit

Permalink
Merge pull request Expensify#54101 from margelo/feat/remove-keyboard-…
Browse files Browse the repository at this point in the history
…coompat-avoiding-view

feat: remove `KeyboardAvoidingView` compat layer
  • Loading branch information
Julesssss authored Dec 17, 2024
2 parents 26b0bd6 + a8600a1 commit 73f071a
Show file tree
Hide file tree
Showing 4 changed files with 15 additions and 166 deletions.
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};

0 comments on commit 73f071a

Please sign in to comment.