Skip to content
Open
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
"date-fns-tz": "^3.2.0",
"dom-serializer": "^0.2.2",
"domhandler": "^5.0.3",
"expensify-common": "2.0.162",
"expensify-common": "2.0.164",
"expo": "54.0.10",
"expo-asset": "12.0.8",
"expo-av": "15.1.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,60 @@
/*
* The KeyboardAvoidingView stub implementation for web and other platforms where the keyboard is handled automatically.
*/
import React from 'react';
import {View} from 'react-native';
import React, {useEffect} from 'react';
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import type {KeyboardAvoidingViewProps} from '@components/KeyboardAvoidingView/types';
import {isMobileSafariOnIos26} from '@libs/Browser';
import KeyboardUtil from '@src/utils/keyboard';

const isMobileSafariIos26 = isMobileSafariOnIos26();

const BUBBLE_DOMAIN_HEIGHT_SAFARI_26 = 15;

function BaseKeyboardAvoidingView(props: KeyboardAvoidingViewProps) {
const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, ...rest} = props;
const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, style, ...rest} = props;
const sharedValue = useSharedValue(0);

const animatedStyle = useAnimatedStyle(() => {
return {paddingBottom: sharedValue.get() * BUBBLE_DOMAIN_HEIGHT_SAFARI_26};
});

useEffect(() => {
if (!isMobileSafariIos26) {
return;
}

let isTiming = false;
let prevIsActive = false;
const handler = (isActive: boolean) => {
if (isTiming && prevIsActive === isActive) {
return;
}
isTiming = true;
prevIsActive = isActive;
sharedValue.set(
withTiming(
isActive ? 1 : 0,
{
duration: 100,
easing: Easing.inOut(Easing.ease),
},
() => {
isTiming = false;
},
),
);
};

return KeyboardUtil.subscribeKeyboardVisibilityChange(handler);
}, [sharedValue]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is affected on all pages which use KeyboardAvoidingView, right?
We should apply this only to report screen which has composer. Otherwise it will cause regressions on other pages on iOS 26 safari.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right.

When I created the PR, I reviewed all the styles applied to BaseKeyboardAvoidingView in the app, and none of them set paddingBottom. So we don't need to worry about our PR overriding any existing paddingBottom.

That said, I still think we should apply the full padding, since in addition to the typing indicator and error messages, there's also the offline indicator. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though paddingBottom prop is not passed in every KeyboardAvoidingView, animatedStyle is always applied, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, but I'm not entirely sure what you mean. The regression has been minimized since we're not overriding any paddingBottom. We still need to apply it globally because of the offline indicator.


return (
// eslint-disable-next-line react/jsx-props-no-spreading
<View {...rest} />
<Animated.View
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
style={[style, isMobileSafariIos26 && animatedStyle]}
/>
);
}

Expand Down
18 changes: 16 additions & 2 deletions src/libs/Browser/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import type {GetBrowser, IsChromeIOS, IsMobile, IsMobileChrome, IsMobileIOS, IsMobileSafari, IsMobileWebKit, IsModernSafari, IsSafari, OpenRouteInDesktopApp} from './types';
import type {
GetBrowser,
IsChromeIOS,
IsMobile,
IsMobileChrome,
IsMobileIOS,
IsMobileSafari,
IsMobileSafariOnIos26,
IsMobileWebKit,
IsModernSafari,
IsSafari,
OpenRouteInDesktopApp,
} from './types';

const getBrowser: GetBrowser = () => '';

Expand All @@ -18,6 +30,8 @@ const isSafari: IsSafari = () => false;

const isModernSafari: IsModernSafari = () => false;

const isMobileSafariOnIos26: IsMobileSafariOnIos26 = () => false;

const openRouteInDesktopApp: OpenRouteInDesktopApp = () => {};

export {getBrowser, isMobile, isMobileIOS, isMobileSafari, isMobileWebKit, isSafari, isModernSafari, isMobileChrome, isChromeIOS, openRouteInDesktopApp};
export {getBrowser, isMobile, isMobileIOS, isMobileSafari, isMobileWebKit, isSafari, isModernSafari, isMobileSafariOnIos26, isMobileChrome, isChromeIOS, openRouteInDesktopApp};
6 changes: 6 additions & 0 deletions src/libs/Browser/index.website.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getOSAndName from '@libs/actions/Device/getDeviceInfo/getOSAndName';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -92,6 +93,10 @@ const isModernSafari: IsModernSafari = (): boolean => {
return parseFloat(iosVersion) >= 18;
};

const isMobileSafariOnIos26: IsModernSafari = (): boolean => {
return isMobileSafari() && getOSAndName().osVersion === '26';
};

/**
* The session information needs to be passed to the Desktop app, and the only way to do that is by using query params. There is no other way to transfer the data.
*/
Expand Down Expand Up @@ -155,4 +160,5 @@ export {
openRouteInDesktopApp,
isOpeningRouteInDesktop,
resetIsOpeningRouteInDesktop,
isMobileSafariOnIos26,
};
4 changes: 3 additions & 1 deletion src/libs/Browser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type IsSafari = () => boolean;

type IsModernSafari = () => boolean;

type IsMobileSafariOnIos26 = () => boolean;

type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string, initialRoute?: string) => void;

export type {GetBrowser, IsMobile, IsMobileIOS, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, IsModernSafari, IsChromeIOS, OpenRouteInDesktopApp};
export type {GetBrowser, IsMobile, IsMobileIOS, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, IsModernSafari, IsMobileSafariOnIos26, IsChromeIOS, OpenRouteInDesktopApp};
7 changes: 6 additions & 1 deletion src/utils/keyboard/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Keyboard.addListener('keyboardDidShow', () => {
isVisible = true;
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const subscribeKeyboardVisibilityChange = (cb: (isVisible: boolean) => void) => {
return () => {};
};

const dismiss = (): Promise<void> => {
return new Promise((resolve) => {
if (!isVisible) {
Expand Down Expand Up @@ -55,7 +60,7 @@ const dismissKeyboardAndExecute = (cb: () => void): Promise<void> => {
});
};

const utils = {dismiss, dismissKeyboardAndExecute};
const utils = {dismiss, dismissKeyboardAndExecute, subscribeKeyboardVisibilityChange};

export type {SimplifiedKeyboardEvent};
export default utils;
7 changes: 6 additions & 1 deletion src/utils/keyboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Keyboard.addListener('keyboardDidShow', () => {
isVisible = true;
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const subscribeKeyboardVisibilityChange = (cb: (isVisible: boolean) => void) => {
return () => {};
};

const dismiss = (): Promise<void> => {
return new Promise((resolve) => {
if (!isVisible) {
Expand All @@ -39,7 +44,7 @@ const dismissKeyboardAndExecute = (cb: () => void): Promise<void> => {
});
};

const utils = {dismiss, dismissKeyboardAndExecute};
const utils = {dismiss, dismissKeyboardAndExecute, subscribeKeyboardVisibilityChange};

export type {SimplifiedKeyboardEvent};
export default utils;
14 changes: 13 additions & 1 deletion src/utils/keyboard/index.website.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import CONST from '@src/CONST';
let isVisible = false;
const initialViewportHeight = window?.visualViewport?.height;

const keyboardVisibilityChangeListenersSet = new Set<(isVisible: boolean) => void>();

const subscribeKeyboardVisibilityChange = (cb: (isVisible: boolean) => void) => {
keyboardVisibilityChangeListenersSet.add(cb);

return () => {
keyboardVisibilityChangeListenersSet.delete(cb);
};
};

const handleResize = () => {
const viewportHeight = window?.visualViewport?.height;

Expand All @@ -16,6 +26,8 @@ const handleResize = () => {
// The 152px threshold accounts for UI elements such as smart banners on iOS Retina (max ~152px)
// and smaller overlays like offline indicators on Android. Height differences > 152px reliably indicate keyboard visibility.
isVisible = initialViewportHeight - viewportHeight > CONST.SMART_BANNER_HEIGHT;

keyboardVisibilityChangeListenersSet.forEach((cb) => cb(isVisible));
};

window.visualViewport?.addEventListener('resize', handleResize);
Expand Down Expand Up @@ -58,6 +70,6 @@ const dismissKeyboardAndExecute = (cb: () => void): Promise<void> => {
});
};

const utils = {dismiss, dismissKeyboardAndExecute};
const utils = {dismiss, dismissKeyboardAndExecute, subscribeKeyboardVisibilityChange};

export default utils;
Loading