diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js index 7caab5e6fb55..c6f9f601f4df 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js @@ -24,7 +24,12 @@ const useEmojiPickerMenu = () => { const [preferredSkinTone] = usePreferredEmojiSkinTone(); const {windowHeight} = useWindowDimensions(); const StyleUtils = useStyleUtils(); - const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight); + /** + * At EmojiPicker has set innerContainerStyle with maxHeight: '95%' by styles.popoverInnerContainer + * to avoid the list style to be cut off due to the list height being larger than the container height + * so we need to calculate listStyle based on the height of the window and innerContainerStyle at the EmojiPicker + */ + const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight * 0.95); useEffect(() => { setFilteredEmojis(allEmojis); diff --git a/src/hooks/useViewportOffsetTop/index.native.ts b/src/hooks/useViewportOffsetTop/index.native.ts new file mode 100644 index 000000000000..b166c360dacc --- /dev/null +++ b/src/hooks/useViewportOffsetTop/index.native.ts @@ -0,0 +1,7 @@ +/** + * Native doesn't support DOM so default value is 0 + */ + +export default function useViewportOffsetTop(): number { + return 0; +} diff --git a/src/hooks/useViewportOffsetTop/index.ts b/src/hooks/useViewportOffsetTop/index.ts new file mode 100644 index 000000000000..56fb19187c4f --- /dev/null +++ b/src/hooks/useViewportOffsetTop/index.ts @@ -0,0 +1,47 @@ +import {useEffect, useRef, useState} from 'react'; +import addViewportResizeListener from '@libs/VisualViewport'; + +/** + * A hook that returns the offset of the top edge of the visual viewport + */ +export default function useViewportOffsetTop(shouldAdjustScrollView = false): number { + const [viewportOffsetTop, setViewportOffsetTop] = useState(0); + const initialHeight = useRef(window.visualViewport?.height ?? window.innerHeight).current; + const cachedDefaultOffsetTop = useRef(0); + useEffect(() => { + const updateOffsetTop = (event?: Event) => { + let targetOffsetTop = window.visualViewport?.offsetTop ?? 0; + if (event?.target instanceof VisualViewport) { + targetOffsetTop = event.target.offsetTop; + } + + if (shouldAdjustScrollView && window.visualViewport) { + const adjustScrollY = Math.round(initialHeight - window.visualViewport.height); + if (cachedDefaultOffsetTop.current === 0) { + cachedDefaultOffsetTop.current = targetOffsetTop; + } + + if (adjustScrollY > targetOffsetTop) { + setViewportOffsetTop(adjustScrollY); + } else if (targetOffsetTop !== 0 && adjustScrollY === targetOffsetTop) { + setViewportOffsetTop(cachedDefaultOffsetTop.current); + } else { + setViewportOffsetTop(targetOffsetTop); + } + } else { + setViewportOffsetTop(targetOffsetTop); + } + }; + updateOffsetTop(); + return addViewportResizeListener(updateOffsetTop); + }, [initialHeight, shouldAdjustScrollView]); + + useEffect(() => { + if (!shouldAdjustScrollView) { + return; + } + window.scrollTo({top: viewportOffsetTop}); + }, [shouldAdjustScrollView, viewportOffsetTop]); + + return viewportOffsetTop; +} diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 9bc0c4c3a4a8..e4a20aa45989 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -21,14 +21,14 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import withCurrentReportID from '@components/withCurrentReportID'; import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; -import withViewportOffsetTop from '@components/withViewportOffsetTop'; -import type {ViewportOffsetTopProps} from '@components/withViewportOffsetTop'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Timing from '@libs/actions/Timing'; +import * as Browser from '@libs/Browser'; import Navigation from '@libs/Navigation/Navigation'; import clearReportNotifications from '@libs/Notification/clearReportNotifications'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; @@ -55,6 +55,9 @@ import {ActionListContext, ReactionListContext} from './ReportScreenContext'; import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext'; type ReportScreenOnyxProps = { + /** Get modal status */ + modal: OnyxEntry; + /** Tells us if the sidebar has rendered */ isSidebarLoaded: OnyxEntry; @@ -93,7 +96,7 @@ type OnyxHOCProps = { type ReportScreenNavigationProps = StackScreenProps; -type ReportScreenProps = OnyxHOCProps & ViewportOffsetTopProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps; +type ReportScreenProps = OnyxHOCProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps; /** Get the currently viewed report ID as number */ function getReportID(route: ReportScreenNavigationProps['route']): string { @@ -131,7 +134,7 @@ function ReportScreen({ markReadyForHydration, policies = {}, isSidebarLoaded = false, - viewportOffsetTop, + modal, isComposerFullSize = false, userLeavingStatus = false, currentReportID = '', @@ -259,6 +262,8 @@ function ReportScreen({ Timing.start(CONST.TIMING.CHAT_RENDER); Performance.markStart(CONST.TIMING.CHAT_RENDER); } + const [isComposerFocus, setIsComposerFocus] = useState(false); + const viewportOffsetTop = useViewportOffsetTop(Browser.isMobileSafari() && isComposerFocus && !modal?.willAlertModalBecomeVisible); const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -644,6 +649,8 @@ function ReportScreen({ {isCurrentReportLoadedFromOnyx ? ( setIsComposerFocus(true)} + onComposerBlur={() => setIsComposerFocus(false)} report={report} pendingAction={reportPendingAction} isComposerFullSize={!!isComposerFullSize} @@ -663,81 +670,82 @@ function ReportScreen({ ReportScreen.displayName = 'ReportScreen'; -export default withViewportOffsetTop( - withCurrentReportID( - withOnyx( - { - isSidebarLoaded: { - key: ONYXKEYS.IS_SIDEBAR_LOADED, - }, - sortedAllReportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - canEvict: false, - selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), - }, - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, - allowStaleData: true, - selector: reportWithoutHasDraftSelector, - }, - reportMetadata: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`, - initialValue: { - isLoadingInitialReportActions: true, - isLoadingOlderReportActions: false, - isLoadingNewerReportActions: false, - }, - }, - isComposerFullSize: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, - initialValue: false, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - allowStaleData: true, - }, - accountManagerReportID: { - key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, - initialValue: null, - }, - userLeavingStatus: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, - initialValue: false, +export default withCurrentReportID( + withOnyx( + { + modal: { + key: ONYXKEYS.MODAL, + }, + isSidebarLoaded: { + key: ONYXKEYS.IS_SIDEBAR_LOADED, + }, + sortedAllReportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + canEvict: false, + selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + }, + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, + allowStaleData: true, + selector: reportWithoutHasDraftSelector, + }, + reportMetadata: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`, + initialValue: { + isLoadingInitialReportActions: true, + isLoadingOlderReportActions: false, + isLoadingNewerReportActions: false, }, - parentReportAction: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, - selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => { - const parentReportActionID = props?.report?.parentReportActionID; - if (!parentReportActionID) { - return null; - } - return parentReportActions?.[parentReportActionID] ?? null; - }, - canEvict: false, + }, + isComposerFullSize: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, + initialValue: false, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + allowStaleData: true, + }, + accountManagerReportID: { + key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, + initialValue: null, + }, + userLeavingStatus: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, + initialValue: false, + }, + parentReportAction: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, + selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => { + const parentReportActionID = props?.report?.parentReportActionID; + if (!parentReportActionID) { + return null; + } + return parentReportActions?.[parentReportActionID] ?? null; }, + canEvict: false, }, - true, - )( - memo( - ReportScreen, - (prevProps, nextProps) => - prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && - lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) && - lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && - prevProps.isComposerFullSize === nextProps.isComposerFullSize && - lodashIsEqual(prevProps.betas, nextProps.betas) && - lodashIsEqual(prevProps.policies, nextProps.policies) && - prevProps.accountManagerReportID === nextProps.accountManagerReportID && - prevProps.userLeavingStatus === nextProps.userLeavingStatus && - prevProps.currentReportID === nextProps.currentReportID && - prevProps.viewportOffsetTop === nextProps.viewportOffsetTop && - lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) && - lodashIsEqual(prevProps.route, nextProps.route) && - lodashIsEqual(prevProps.report, nextProps.report), - ), + }, + true, + )( + memo( + ReportScreen, + (prevProps, nextProps) => + prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && + lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) && + lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && + prevProps.isComposerFullSize === nextProps.isComposerFullSize && + lodashIsEqual(prevProps.betas, nextProps.betas) && + lodashIsEqual(prevProps.policies, nextProps.policies) && + prevProps.accountManagerReportID === nextProps.accountManagerReportID && + prevProps.userLeavingStatus === nextProps.userLeavingStatus && + prevProps.currentReportID === nextProps.currentReportID && + lodashIsEqual(prevProps.modal, nextProps.modal) && + lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) && + lodashIsEqual(prevProps.route, nextProps.route) && + lodashIsEqual(prevProps.report, nextProps.report), ), ), ); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 1e0e322be258..70340e9e1fec 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -85,6 +85,12 @@ type ReportActionComposeProps = ReportActionComposeOnyxProps & /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; + + /** A method to call when the input is focus */ + onComposerFocus?: () => void; + + /** A method to call when the input is blur */ + onComposerBlur?: () => void; }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will @@ -107,6 +113,8 @@ function ReportActionCompose({ isReportReadyForDisplay = true, isEmptyChat, lastReportAction, + onComposerFocus, + onComposerBlur, }: ReportActionComposeProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -293,20 +301,25 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = true; }, []); - const onBlur = useCallback((event: NativeSyntheticEvent) => { - const webEvent = event as unknown as FocusEvent; - setIsFocused(false); - if (suggestionsRef.current) { - suggestionsRef.current.resetSuggestions(); - } - if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) { - isKeyboardVisibleWhenShowingModalRef.current = true; - } - }, []); + const onBlur = useCallback( + (event: NativeSyntheticEvent) => { + const webEvent = event as unknown as FocusEvent; + setIsFocused(false); + onComposerBlur?.(); + if (suggestionsRef.current) { + suggestionsRef.current.resetSuggestions(); + } + if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) { + isKeyboardVisibleWhenShowingModalRef.current = true; + } + }, + [onComposerBlur], + ); const onFocus = useCallback(() => { setIsFocused(true); - }, []); + onComposerFocus?.(); + }, [onComposerFocus]); // resets the composer to normal size when // the send button is pressed. diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 3a787e1dbd0f..bd143f9ef196 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -51,6 +51,12 @@ type ReportFooterProps = ReportFooterOnyxProps & { /** Whether the composer is in full size */ isComposerFullSize?: boolean; + + /** A method to call when the input is focus */ + onComposerFocus: () => void; + + /** A method to call when the input is blur */ + onComposerBlur: () => void; }; function ReportFooter({ @@ -63,6 +69,8 @@ function ReportFooter({ isReportReadyForDisplay = true, listHeight = 0, isComposerFullSize = false, + onComposerBlur, + onComposerFocus, }: ReportFooterProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -137,6 +145,8 @@ function ReportFooter({