From 1b5c5902d1344fcb5ba6071c39c22f49ae9f7c40 Mon Sep 17 00:00:00 2001 From: christianwen Date: Mon, 21 Oct 2024 17:50:19 +0700 Subject: [PATCH 01/62] fix: 10371 auto focus input --- .../Composer/implementation/index.native.tsx | 17 +- .../Composer/implementation/index.tsx | 40 ++-- src/components/Composer/types.ts | 3 + src/pages/home/ReportScreen.tsx | 2 - .../ComposerWithSuggestions.tsx | 204 +++++++----------- .../ReportActionCompose.tsx | 13 +- src/pages/home/report/ReportFooter.tsx | 6 - 7 files changed, 122 insertions(+), 163 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 9f237dd02424..4de6e9280401 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -1,7 +1,7 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import mimeDb from 'mime-db'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native'; import {StyleSheet} from 'react-native'; import type {FileObject} from '@components/AttachmentModal'; @@ -9,6 +9,7 @@ import type {ComposerProps} from '@components/Composer/types'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useKeyboardState from '@hooks/useKeyboardState'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -38,6 +39,7 @@ function Composer( selection, value, isGroupPolicyReport = false, + showSoftInputOnFocus = true, ...props }: ComposerProps, ref: ForwardedRef, @@ -50,7 +52,11 @@ function Composer( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const [contextMenuHidden, setContextMenuHidden] = useState(true); + const {inputCallbackRef, inputRef: autoFocusInputRef} = useAutoFocusInput(); + const keyboardState = useKeyboardState(); + const isKeyboardShown = keyboardState?.isKeyboardShown ?? false; useEffect(() => { if (autoFocus === !!autoFocusInputRef.current) { @@ -59,6 +65,13 @@ function Composer( inputCallbackRef(autoFocus ? textInput.current : null); }, [autoFocus, inputCallbackRef, autoFocusInputRef]); + useEffect(() => { + if (!showSoftInputOnFocus || !isKeyboardShown) { + return; + } + setContextMenuHidden(false); + }, [showSoftInputOnFocus, isKeyboardShown]); + /** * Set the TextInput Ref * @param {Element} el @@ -137,6 +150,8 @@ function Composer( props?.onBlur?.(e); }} onClear={onClear} + showSoftInputOnFocus={showSoftInputOnFocus} + contextMenuHidden={contextMenuHidden} /> ); } diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 4431007793cb..838a2f6a869d 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -50,6 +50,7 @@ function Composer( isComposerFullSize = false, shouldContainScroll = true, isGroupPolicyReport = false, + showSoftInputOnFocus = true, ...props }: ComposerProps, ref: ForwardedRef, @@ -280,28 +281,24 @@ function Composer( onClear(currentText); }, [onClear, onSelectionChange]); - useImperativeHandle( - ref, - () => { - const textInputRef = textInput.current; - if (!textInputRef) { - throw new Error('textInputRef is not available. This should never happen and indicates a developer error.'); - } + useImperativeHandle(ref, () => { + const textInputRef = textInput.current; + if (!textInputRef) { + throw new Error('textInputRef is not available. This should never happen and indicates a developer error.'); + } - return { - ...textInputRef, - // Overwrite clear with our custom implementation, which mimics how the native TextInput's clear method works - clear, - // We have to redefine these methods as they are inherited by prototype chain and are not accessible directly - blur: () => textInputRef.blur(), - focus: () => textInputRef.focus(), - get scrollTop() { - return textInputRef.scrollTop; - }, - }; - }, - [clear], - ); + return { + ...textInputRef, + // Overwrite clear with our custom implementation, which mimics how the native TextInput's clear method works + clear, + // We have to redefine these methods as they are inherited by prototype chain and are not accessible directly + blur: () => textInputRef.blur(), + focus: () => textInputRef.focus(), + get scrollTop() { + return textInputRef.scrollTop; + }, + }; + }, [clear]); const handleKeyPress = useCallback( (e: NativeSyntheticEvent) => { @@ -349,6 +346,7 @@ function Composer( value={value} defaultValue={defaultValue} autoFocus={autoFocus} + inputMode={showSoftInputOnFocus ? 'text' : 'none'} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index ef497dd52e47..7f54c7486e8d 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -74,6 +74,9 @@ type ComposerProps = Omit & { /** Indicates whether the composer is in a group policy report. Used for disabling report mentioning style in markdown input */ isGroupPolicyReport?: boolean; + + /** Whether the soft keyboard is open */ + showSoftInputOnFocus?: boolean; }; export type {TextSelection, ComposerProps, CustomSelectionChangeEvent}; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 4a87d51e3c82..ed4a47f4ff27 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -238,7 +238,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; - const isEmptyChat = useMemo(() => ReportUtils.isEmptyReport(report), [report]); const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const indexOfLinkedMessage = useMemo( (): number => reportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)), @@ -811,7 +810,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro policy={policy} pendingAction={reportPendingAction} isComposerFullSize={!!isComposerFullSize} - isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} workspaceTooltip={workspaceTooltip} /> diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index e63bd952b4ab..12b145a78e87 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,6 +1,6 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; -import type {ForwardedRef, MutableRefObject, RefAttributes, RefObject} from 'react'; +import type {ForwardedRef, MutableRefObject, RefObject} from 'react'; import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type { LayoutChangeEvent, @@ -14,7 +14,7 @@ import type { import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import {useFocusedInputHandler} from 'react-native-keyboard-controller'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import {useAnimatedRef, useSharedValue} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; @@ -29,7 +29,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; -import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {forceClearInput} from '@libs/ComponentUtils'; import * as ComposerUtils from '@libs/ComposerUtils'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; @@ -40,7 +39,6 @@ import getPlatform from '@libs/getPlatform'; import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener'; import Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; @@ -65,113 +63,85 @@ type SyncSelection = { type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; -type ComposerWithSuggestionsOnyxProps = { - /** The parent report actions for the report */ - parentReportActions: OnyxEntry; +type ComposerWithSuggestionsProps = Partial & { + /** Report ID */ + reportID: string; - /** The modal state */ - modal: OnyxEntry; + /** Callback to focus composer */ + onFocus: () => void; - /** The preferred skin tone of the user */ - preferredSkinTone: number; + /** Callback to blur composer */ + onBlur: (event: NativeSyntheticEvent) => void; - /** Whether the input is focused */ - editFocused: OnyxEntry; -}; - -type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & - Partial & { - /** Report ID */ - reportID: string; - - /** Callback to focus composer */ - onFocus: () => void; - - /** Callback to blur composer */ - onBlur: (event: NativeSyntheticEvent) => void; - - /** Callback when layout of composer changes */ - onLayout?: (event: LayoutChangeEvent) => void; - - /** Callback to update the value of the composer */ - onValueChange: (value: string) => void; + /** Callback when layout of composer changes */ + onLayout?: (event: LayoutChangeEvent) => void; - /** Callback when the composer got cleared on the UI thread */ - onCleared?: (text: string) => void; + /** Callback to update the value of the composer */ + onValueChange: (value: string) => void; - /** Whether the composer is full size */ - isComposerFullSize: boolean; + /** Callback when the composer got cleared on the UI thread */ + onCleared?: (text: string) => void; - /** Whether the menu is visible */ - isMenuVisible: boolean; + /** Whether the composer is full size */ + isComposerFullSize: boolean; - /** The placeholder for the input */ - inputPlaceholder: string; + /** Whether the menu is visible */ + isMenuVisible: boolean; - /** Function to display a file in a modal */ - displayFileInModal: (file: FileObject) => void; + /** The placeholder for the input */ + inputPlaceholder: string; - /** Whether the user is blocked from concierge */ - isBlockedFromConcierge: boolean; + /** Function to display a file in a modal */ + displayFileInModal: (file: FileObject) => void; - /** Whether the input is disabled */ - disabled: boolean; + /** Whether the user is blocked from concierge */ + isBlockedFromConcierge: boolean; - /** Whether the full composer is available */ - isFullComposerAvailable: boolean; + /** Whether the input is disabled */ + disabled: boolean; - /** Function to set whether the full composer is available */ - setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + /** Whether the full composer is available */ + isFullComposerAvailable: boolean; - /** Function to set whether the comment is empty */ - setIsCommentEmpty: (isCommentEmpty: boolean) => void; + /** Function to set whether the full composer is available */ + setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; - /** Function to handle sending a message */ - handleSendMessage: () => void; + /** Function to set whether the comment is empty */ + setIsCommentEmpty: (isCommentEmpty: boolean) => void; - /** Whether the compose input should show */ - shouldShowComposeInput: OnyxEntry; + /** Function to handle sending a message */ + handleSendMessage: () => void; - /** Function to measure the parent container */ - measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + /** Whether the compose input should show */ + shouldShowComposeInput: OnyxEntry; - /** Whether the scroll is likely to trigger a layout */ - isScrollLikelyLayoutTriggered: RefObject; + /** Function to measure the parent container */ + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; - /** Function to raise the scroll is likely layout triggered */ - raiseIsScrollLikelyLayoutTriggered: () => void; + /** Whether the scroll is likely to trigger a layout */ + isScrollLikelyLayoutTriggered: RefObject; - /** The ref to the suggestions */ - suggestionsRef: React.RefObject; + /** Function to raise the scroll is likely layout triggered */ + raiseIsScrollLikelyLayoutTriggered: () => void; - /** The ref to the next modal will open */ - isNextModalWillOpenRef: MutableRefObject; + /** The ref to the suggestions */ + suggestionsRef: React.RefObject; - /** Whether the edit is focused */ - editFocused: boolean; + /** The ref to the next modal will open */ + isNextModalWillOpenRef: MutableRefObject; - /** Wheater chat is empty */ - isEmptyChat?: boolean; + /** The last report action */ + lastReportAction?: OnyxEntry; - /** The last report action */ - lastReportAction?: OnyxEntry; + /** Whether to include chronos */ + includeChronos?: boolean; - /** Whether to include chronos */ - includeChronos?: boolean; + /** Whether report is from group policy */ + isGroupPolicyReport: boolean; - /** The parent report action ID */ - parentReportActionID?: string; - - /** The parent report ID */ - // eslint-disable-next-line react/no-unused-prop-types -- its used in the withOnyx HOC - parentReportID: string | undefined; - - /** Whether report is from group policy */ - isGroupPolicyReport: boolean; - - /** policy ID of the report */ - policyID: string; - }; + /** policy ID of the report */ + policyID: string; +}; type SwitchToCurrentReportProps = { preexistingReportID: string; @@ -211,10 +181,6 @@ const debouncedBroadcastUserIsTyping = lodashDebounce( const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); -// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will -// prevent auto focus on existing chat for mobile device -const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - /** * This component holds the value and selection state. * If a component really needs access to these state values it should be put here. @@ -223,17 +189,10 @@ const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); */ function ComposerWithSuggestions( { - // Onyx - modal, - preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, - parentReportActions, - // Props: Report reportID, includeChronos, - isEmptyChat, lastReportAction, - parentReportActionID, isGroupPolicyReport, policyID, @@ -263,7 +222,6 @@ function ComposerWithSuggestions( // Refs suggestionsRef, isNextModalWillOpenRef, - editFocused, // For testing children, @@ -288,6 +246,15 @@ function ComposerWithSuggestions( } return draftComment; }); + + const [modal] = useOnyx(ONYXKEYS.MODAL); + const [preferredSkinTone] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, { + selector: EmojiUtils.getPreferredSkinToneIndex, + initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, + }); + + const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED); + const commentRef = useRef(value); const lastTextRef = useRef(value); @@ -298,13 +265,7 @@ function ComposerWithSuggestions( const {shouldUseNarrowLayout} = useResponsiveLayout(); const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const parentReportAction = parentReportActions?.[parentReportActionID ?? '-1']; - const shouldAutoFocus = - !modal?.isVisible && - Modal.areAllModalsHidden() && - isFocused && - (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && - shouldShowComposeInput; + const shouldAutoFocus = !modal?.isVisible && shouldShowComposeInput && Modal.areAllModalsHidden() && isFocused; const valueRef = useRef(value); valueRef.current = value; @@ -313,6 +274,8 @@ function ComposerWithSuggestions( const [composerHeight, setComposerHeight] = useState(0); + const [showSoftInputOnFocus, setShowSoftInputOnFocus] = useState(false); + const textInputRef = useRef(null); const syncSelectionWithOnChangeTextRef = useRef(null); @@ -800,6 +763,19 @@ function ComposerWithSuggestions( onScroll={hideSuggestionMenu} shouldContainScroll={Browser.isMobileSafari()} isGroupPolicyReport={isGroupPolicyReport} + showSoftInputOnFocus={showSoftInputOnFocus} + onTouchStart={() => { + if (showSoftInputOnFocus) { + return; + } + if (Browser.isMobileSafari()) { + setTimeout(() => { + setShowSoftInputOnFocus(true); + }, CONST.ANIMATED_TRANSITION); + return; + } + setShowSoftInputOnFocus(true); + }} /> @@ -837,22 +813,6 @@ ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); -export default withOnyx, ComposerWithSuggestionsOnyxProps>({ - modal: { - key: ONYXKEYS.MODAL, - }, - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - selector: EmojiUtils.getPreferredSkinToneIndex, - }, - editFocused: { - key: ONYXKEYS.INPUT_FOCUSED, - }, - parentReportActions: { - key: ({parentReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, - canEvict: false, - initWithStoredValues: false, - }, -})(memo(ComposerWithSuggestionsWithRef)); +export default memo(ComposerWithSuggestionsWithRef); export type {ComposerWithSuggestionsProps, ComposerRef}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 9d34fe86c092..f469d91dbf2d 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -28,7 +28,6 @@ import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import DomUtils from '@libs/DomUtils'; import {getDraftComment} from '@libs/DraftCommentUtils'; @@ -64,7 +63,7 @@ type SuggestionsRef = { getIsSuggestionsMenuVisible: () => boolean; }; -type ReportActionComposeProps = Pick & { +type ReportActionComposeProps = Pick & { /** A method to call when the form is submitted */ onSubmit: (newComment: string) => void; @@ -90,10 +89,6 @@ type ReportActionComposeProps = Pick { const initialModalState = getModalState(); - return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible; + return shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible; }); const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize); const [shouldHideEducationalTooltip, setShouldHideEducationalTooltip] = useState(false); @@ -468,11 +462,8 @@ function ReportActionCompose({ raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered} reportID={reportID} policyID={report?.policyID ?? '-1'} - parentReportID={report?.parentReportID} - parentReportActionID={report?.parentReportActionID} includeChronos={ReportUtils.chatIncludesChronos(report)} isGroupPolicyReport={isGroupPolicyReport} - isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} isMenuVisible={isMenuVisible} inputPlaceholder={inputPlaceholder} diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 7c4ec786b633..90746efa3b68 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -48,9 +48,6 @@ type ReportFooterProps = { /** Whether to show educational tooltip in workspace chat for first-time user */ workspaceTooltip: OnyxEntry; - /** Whether the chat is empty */ - isEmptyChat?: boolean; - /** The pending action when we are adding a chat */ pendingAction?: PendingAction; @@ -73,7 +70,6 @@ function ReportFooter({ report = {reportID: '-1'}, reportMetadata, policy, - isEmptyChat = true, isReportReadyForDisplay = true, isComposerFullSize = false, workspaceTooltip, @@ -224,7 +220,6 @@ function ReportFooter({ onComposerBlur={onComposerBlur} reportID={report.reportID} report={report} - isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} pendingAction={pendingAction} isComposerFullSize={isComposerFullSize} @@ -246,7 +241,6 @@ export default memo( lodashIsEqual(prevProps.report, nextProps.report) && prevProps.pendingAction === nextProps.pendingAction && prevProps.isComposerFullSize === nextProps.isComposerFullSize && - prevProps.isEmptyChat === nextProps.isEmptyChat && prevProps.lastReportAction === nextProps.lastReportAction && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay && prevProps.workspaceTooltip?.shouldShow === nextProps.workspaceTooltip?.shouldShow && From 1af3ada68d4a1f75c3798773b93b74fbbb7af1e0 Mon Sep 17 00:00:00 2001 From: christianwen Date: Mon, 21 Oct 2024 17:54:13 +0700 Subject: [PATCH 02/62] refactor --- .../Composer/implementation/index.tsx | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 838a2f6a869d..e40bb716e0a0 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -281,24 +281,28 @@ function Composer( onClear(currentText); }, [onClear, onSelectionChange]); - useImperativeHandle(ref, () => { - const textInputRef = textInput.current; - if (!textInputRef) { - throw new Error('textInputRef is not available. This should never happen and indicates a developer error.'); - } + useImperativeHandle( + ref, + () => { + const textInputRef = textInput.current; + if (!textInputRef) { + throw new Error('textInputRef is not available. This should never happen and indicates a developer error.'); + } - return { - ...textInputRef, - // Overwrite clear with our custom implementation, which mimics how the native TextInput's clear method works - clear, - // We have to redefine these methods as they are inherited by prototype chain and are not accessible directly - blur: () => textInputRef.blur(), - focus: () => textInputRef.focus(), - get scrollTop() { - return textInputRef.scrollTop; - }, - }; - }, [clear]); + return { + ...textInputRef, + // Overwrite clear with our custom implementation, which mimics how the native TextInput's clear method works + clear, + // We have to redefine these methods as they are inherited by prototype chain and are not accessible directly + blur: () => textInputRef.blur(), + focus: () => textInputRef.focus(), + get scrollTop() { + return textInputRef.scrollTop; + }, + }; + }, + [clear], + ); const handleKeyPress = useCallback( (e: NativeSyntheticEvent) => { From 62513eef4806ad194c134b38644a41206c256afc Mon Sep 17 00:00:00 2001 From: christianwen Date: Thu, 31 Oct 2024 14:48:36 +0700 Subject: [PATCH 03/62] fix bugs flicker --- src/pages/home/ReportScreen.tsx | 5 ++++- .../ComposerWithSuggestions.tsx | 10 ++++++++-- .../ReportActionCompose/ReportActionCompose.tsx | 10 ++++++++++ src/pages/home/report/ReportFooter.tsx | 12 ++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 1027f4c05d3c..5de2652632a8 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -266,6 +266,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`]; const isTopMostReportId = currentReportID === reportIDFromRoute; const didSubscribeToReportLeavingEvents = useRef(false); + const [showSoftInputOnFocus, setShowSoftInputOnFocus] = useState(false); useEffect(() => { if (!report?.reportID || shouldHideReport) { @@ -711,7 +712,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro ) : null} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 12b145a78e87..41fd13aff99b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -141,6 +141,12 @@ type ComposerWithSuggestionsProps = Partial & { /** policy ID of the report */ policyID: string; + + /** Whether the soft keyboard is open */ + showSoftInputOnFocus: boolean; + + /** A method to update showSoftInputOnFocus */ + setShowSoftInputOnFocus: (value: boolean) => void; }; type SwitchToCurrentReportProps = { @@ -225,6 +231,8 @@ function ComposerWithSuggestions( // For testing children, + showSoftInputOnFocus, + setShowSoftInputOnFocus, }: ComposerWithSuggestionsProps, ref: ForwardedRef, ) { @@ -274,8 +282,6 @@ function ComposerWithSuggestions( const [composerHeight, setComposerHeight] = useState(0); - const [showSoftInputOnFocus, setShowSoftInputOnFocus] = useState(false); - const textInputRef = useRef(null); const syncSelectionWithOnChangeTextRef = useRef(null); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index aee324cd745f..16973a5cea41 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -87,6 +87,12 @@ type ReportActionComposeProps = Pick void; }; const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); @@ -104,8 +110,10 @@ function ReportActionCompose({ isReportReadyForDisplay = true, lastReportAction, shouldShowEducationalTooltip, + showSoftInputOnFocus, onComposerFocus, onComposerBlur, + setShowSoftInputOnFocus, }: ReportActionComposeProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -487,6 +495,8 @@ function ReportActionCompose({ } validateCommentMaxLength(value, {reportID}); }} + showSoftInputOnFocus={showSoftInputOnFocus} + setShowSoftInputOnFocus={setShowSoftInputOnFocus} /> { diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index ac2456a5b4d5..1e782e4acdee 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -62,6 +62,12 @@ type ReportFooterProps = { /** A method to call when the input is blur */ onComposerBlur: () => void; + + /** Whether the soft keyboard is open */ + showSoftInputOnFocus: boolean; + + /** A method to update showSoftInputOnFocus */ + setShowSoftInputOnFocus: (value: boolean) => void; }; function ReportFooter({ @@ -73,8 +79,10 @@ function ReportFooter({ isReportReadyForDisplay = true, isComposerFullSize = false, workspaceTooltip, + showSoftInputOnFocus, onComposerBlur, onComposerFocus, + setShowSoftInputOnFocus, }: ReportFooterProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -179,6 +187,7 @@ function ReportFooter({ [report.reportID, handleCreateTask], ); + console.log('9999', showSoftInputOnFocus); return ( <> {!!shouldHideComposer && ( @@ -225,6 +234,8 @@ function ReportFooter({ isComposerFullSize={isComposerFullSize} isReportReadyForDisplay={isReportReadyForDisplay} shouldShowEducationalTooltip={didScreenTransitionEnd && shouldShowEducationalTooltip} + showSoftInputOnFocus={showSoftInputOnFocus} + setShowSoftInputOnFocus={setShowSoftInputOnFocus} /> @@ -244,6 +255,7 @@ export default memo( prevProps.lastReportAction === nextProps.lastReportAction && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay && prevProps.workspaceTooltip?.shouldShow === nextProps.workspaceTooltip?.shouldShow && + prevProps.showSoftInputOnFocus === nextProps.showSoftInputOnFocus && lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && lodashIsEqual(prevProps.policy?.employeeList, nextProps.policy?.employeeList) && lodashIsEqual(prevProps.policy?.role, nextProps.policy?.role), From e4823f07f53dc85f09453d56f687fb5c54825c1f Mon Sep 17 00:00:00 2001 From: christianwen Date: Thu, 31 Oct 2024 14:52:01 +0700 Subject: [PATCH 04/62] remove console --- src/pages/home/report/ReportFooter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 1e782e4acdee..1e5a2f4c0283 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -187,7 +187,6 @@ function ReportFooter({ [report.reportID, handleCreateTask], ); - console.log('9999', showSoftInputOnFocus); return ( <> {!!shouldHideComposer && ( From f8c18261fea2fb263d8638005fb8dba4e6bd3c34 Mon Sep 17 00:00:00 2001 From: christianwen Date: Thu, 31 Oct 2024 15:02:07 +0700 Subject: [PATCH 05/62] fix ts --- tests/perf-test/ReportActionCompose.perf-test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx index 845727c75c97..1827e23ffe4b 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.tsx +++ b/tests/perf-test/ReportActionCompose.perf-test.tsx @@ -96,6 +96,8 @@ function ReportActionComposeWrapper() { disabled={false} report={LHNTestUtils.getFakeReport()} isComposerFullSize + showSoftInputOnFocus={false} + setShowSoftInputOnFocus={() => {}} /> ); From 50965a73228416b21856b56fdc8dc820f880aadd Mon Sep 17 00:00:00 2001 From: christianwen Date: Mon, 18 Nov 2024 14:45:18 +0700 Subject: [PATCH 06/62] prevent refocus on closing modal --- .../ComposerWithSuggestions.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 582aabf06c9e..210d3e17bc7d 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -29,6 +29,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; +import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {forceClearInput} from '@libs/ComponentUtils'; import * as ComposerUtils from '@libs/ComposerUtils'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; @@ -624,7 +625,15 @@ function ComposerWithSuggestions( // We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused. // We avoid doing this on native platforms since the software keyboard popping // open creates a jarring and broken UX. - if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal?.isVisible && isFocused && (!!prevIsModalVisible || !prevIsFocused))) { + if ( + !( + (willBlurTextInputOnTapOutside || (shouldAutoFocus && canFocusInputOnScreenFocus())) && + !isNextModalWillOpenRef.current && + !modal?.isVisible && + isFocused && + (!!prevIsModalVisible || !prevIsFocused) + ) + ) { return; } From 857262238f1518f5c0ac0461dccf5d1844bb8330 Mon Sep 17 00:00:00 2001 From: christianwen Date: Wed, 20 Nov 2024 15:53:25 +0700 Subject: [PATCH 07/62] fix cursor flash --- .../Composer/implementation/index.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index bf155bfdc04b..cdf43ecd8d90 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -4,7 +4,7 @@ import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports import type {NativeSyntheticEvent, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; -import {DeviceEventEmitter, StyleSheet} from 'react-native'; +import {DeviceEventEmitter, InteractionManager, StyleSheet} from 'react-native'; import type {ComposerProps} from '@components/Composer/types'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; @@ -72,6 +72,11 @@ function Composer( end: selectionProp.end, }); const [isRendered, setIsRendered] = useState(false); + + // On mobile safari, the cursor will move from right to left with inputMode set to none during report transition + // To avoid that we should hide the cursor util the transition is finished + const [shouldTransparentCursor, setShouldTransparentCursor] = useState(!showSoftInputOnFocus && Browser.isMobileSafari()); + const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); const isReportFlatListScrolling = useRef(false); @@ -256,6 +261,15 @@ function Composer( setIsRendered(true); }, []); + useEffect(() => { + if (!shouldTransparentCursor) { + return; + } + InteractionManager.runAfterInteractions(() => { + setShouldTransparentCursor(false); + }); + }, [shouldTransparentCursor]); + const clear = useCallback(() => { if (!textInput.current) { return; @@ -343,7 +357,7 @@ function Composer( placeholderTextColor={theme.placeholderText} ref={(el) => (textInput.current = el)} selection={selection} - style={[inputStyleMemo]} + style={[inputStyleMemo, shouldTransparentCursor ? {color: 'transparent'} : undefined]} markdownStyle={markdownStyle} value={value} defaultValue={defaultValue} From 6019f6465f5ed94c18fbe2205784ff2b568698f8 Mon Sep 17 00:00:00 2001 From: christianwen Date: Mon, 25 Nov 2024 15:04:56 +0700 Subject: [PATCH 08/62] transparent caret safari --- src/components/Composer/implementation/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index aa7ab0f87898..3a78d9fda0a5 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -359,7 +359,7 @@ function Composer( placeholderTextColor={theme.placeholderText} ref={(el) => (textInput.current = el)} selection={selection} - style={[inputStyleMemo, shouldTransparentCursor ? {color: 'transparent'} : undefined]} + style={[inputStyleMemo, shouldTransparentCursor ? {caretColor: 'transparent'} : undefined]} markdownStyle={markdownStyle} value={value} defaultValue={defaultValue} From 8e2d09b1dabbd8e4614fc140a742b4a8b84931e6 Mon Sep 17 00:00:00 2001 From: christianwen Date: Mon, 2 Dec 2024 12:29:30 +0700 Subject: [PATCH 09/62] fix: composer hide chat --- src/pages/home/ReportScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index d3e03360ac4e..bd2207f9e910 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -749,7 +749,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro Date: Tue, 3 Dec 2024 14:13:48 +0700 Subject: [PATCH 10/62] prevent submit button from jumping when proceeding to the confirmation page --- .../step/IOURequestStepParticipants.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index fb2484ea414f..4552e94b3b5d 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -18,6 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Participant} from '@src/types/onyx/IOU'; +import KeyboardUtils from '@src/utils/keyboard'; import StepScreenWrapper from './StepScreenWrapper'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; @@ -131,11 +132,14 @@ function IOURequestStepParticipants({ transactionID, selectedReportID.current || reportID, ); - if (isCategorizing) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute)); - } else { - Navigation.navigate(iouConfirmationPageRoute); - } + + const route = isCategorizing + ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute) + : iouConfirmationPageRoute; + + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(route); + }); }, [iouType, transactionID, transaction, reportID, action, participants]); const navigateBack = useCallback(() => { @@ -153,7 +157,9 @@ function IOURequestStepParticipants({ IOU.setCustomUnitRateID(transactionID, rateID); IOU.setMoneyRequestParticipantsFromReport(transactionID, ReportUtils.getReport(selfDMReportID)); const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); - Navigation.navigate(iouConfirmationPageRoute); + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(iouConfirmationPageRoute); + }); }; useEffect(() => { From c0d30f792930f1757d933f7370a618876b0f1335 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:04:24 +0700 Subject: [PATCH 11/62] Update dismiss keyboard for web --- src/utils/keyboard.ts | 83 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/src/utils/keyboard.ts b/src/utils/keyboard.ts index a2b1d329aa0a..6cea7dc727cf 100644 --- a/src/utils/keyboard.ts +++ b/src/utils/keyboard.ts @@ -1,25 +1,82 @@ -import {Keyboard} from 'react-native'; +import {InteractionManager, Keyboard} from 'react-native'; +import getPlatform from '@libs/getPlatform'; +import CONST from '@src/CONST'; -let isVisible = false; +let isNativeKeyboardVisible = false; // Native keyboard visibility +let isWebKeyboardOpen = false; // Web keyboard visibility +const isWeb = getPlatform() === CONST.PLATFORM.WEB; +/** + * Initializes native keyboard visibility listeners + */ +const initializeNativeKeyboardListeners = () => { + Keyboard.addListener('keyboardDidHide', () => { + isNativeKeyboardVisible = false; + }); + + Keyboard.addListener('keyboardDidShow', () => { + isNativeKeyboardVisible = true; + }); +}; + +/** + * Checks if the given HTML element is a keyboard-related input + */ +const isKeyboardInput = (elem: HTMLElement): boolean => + (elem.tagName === 'INPUT' && !['button', 'submit', 'checkbox', 'file', 'image'].includes((elem as HTMLInputElement).type)) || elem.hasAttribute('contenteditable'); -Keyboard.addListener('keyboardDidHide', () => { - isVisible = false; -}); +/** + * Initializes web-specific keyboard visibility listeners + */ +const initializeWebKeyboardListeners = () => { + if (typeof document === 'undefined' || !isWeb) { + return; + } + + const handleFocusIn = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isWebKeyboardOpen = true; + } + }; + + const handleFocusOut = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isWebKeyboardOpen = false; + } + }; -Keyboard.addListener('keyboardDidShow', () => { - isVisible = true; -}); + document.addEventListener('focusin', handleFocusIn); + document.addEventListener('focusout', handleFocusOut); +}; +/** + * Dismisses the keyboard and resolves the promise when the dismissal is complete + */ const dismiss = (): Promise => { return new Promise((resolve) => { - if (!isVisible) { - resolve(); + if (isWeb) { + if (!isWebKeyboardOpen) { + resolve(); + return; + } + + Keyboard.dismiss(); + InteractionManager.runAfterInteractions(() => { + isWebKeyboardOpen = false; + resolve(); + }); return; } + if (!isNativeKeyboardVisible) { + resolve(); + return; + } + const subscription = Keyboard.addListener('keyboardDidHide', () => { - resolve(undefined); + resolve(); subscription.remove(); }); @@ -27,6 +84,10 @@ const dismiss = (): Promise => { }); }; +// Initialize listeners for native and web +initializeNativeKeyboardListeners(); +initializeWebKeyboardListeners(); + const utils = {dismiss}; export default utils; From ff74004bb4fec9c63b3cadf343da23746b45dcb4 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:08:02 +0700 Subject: [PATCH 12/62] Add keyboard dismiss for web --- src/utils/keyboard.ts | 93 ----------------------------- src/utils/keyboard/index.ts | 32 ++++++++++ src/utils/keyboard/index.website.ts | 46 ++++++++++++++ 3 files changed, 78 insertions(+), 93 deletions(-) delete mode 100644 src/utils/keyboard.ts create mode 100644 src/utils/keyboard/index.ts create mode 100644 src/utils/keyboard/index.website.ts diff --git a/src/utils/keyboard.ts b/src/utils/keyboard.ts deleted file mode 100644 index 6cea7dc727cf..000000000000 --- a/src/utils/keyboard.ts +++ /dev/null @@ -1,93 +0,0 @@ -import {InteractionManager, Keyboard} from 'react-native'; -import getPlatform from '@libs/getPlatform'; -import CONST from '@src/CONST'; - -let isNativeKeyboardVisible = false; // Native keyboard visibility -let isWebKeyboardOpen = false; // Web keyboard visibility -const isWeb = getPlatform() === CONST.PLATFORM.WEB; -/** - * Initializes native keyboard visibility listeners - */ -const initializeNativeKeyboardListeners = () => { - Keyboard.addListener('keyboardDidHide', () => { - isNativeKeyboardVisible = false; - }); - - Keyboard.addListener('keyboardDidShow', () => { - isNativeKeyboardVisible = true; - }); -}; - -/** - * Checks if the given HTML element is a keyboard-related input - */ -const isKeyboardInput = (elem: HTMLElement): boolean => - (elem.tagName === 'INPUT' && !['button', 'submit', 'checkbox', 'file', 'image'].includes((elem as HTMLInputElement).type)) || elem.hasAttribute('contenteditable'); - -/** - * Initializes web-specific keyboard visibility listeners - */ -const initializeWebKeyboardListeners = () => { - if (typeof document === 'undefined' || !isWeb) { - return; - } - - const handleFocusIn = (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isWebKeyboardOpen = true; - } - }; - - const handleFocusOut = (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isWebKeyboardOpen = false; - } - }; - - document.addEventListener('focusin', handleFocusIn); - document.addEventListener('focusout', handleFocusOut); -}; - -/** - * Dismisses the keyboard and resolves the promise when the dismissal is complete - */ -const dismiss = (): Promise => { - return new Promise((resolve) => { - if (isWeb) { - if (!isWebKeyboardOpen) { - resolve(); - return; - } - - Keyboard.dismiss(); - InteractionManager.runAfterInteractions(() => { - isWebKeyboardOpen = false; - resolve(); - }); - - return; - } - - if (!isNativeKeyboardVisible) { - resolve(); - return; - } - - const subscription = Keyboard.addListener('keyboardDidHide', () => { - resolve(); - subscription.remove(); - }); - - Keyboard.dismiss(); - }); -}; - -// Initialize listeners for native and web -initializeNativeKeyboardListeners(); -initializeWebKeyboardListeners(); - -const utils = {dismiss}; - -export default utils; diff --git a/src/utils/keyboard/index.ts b/src/utils/keyboard/index.ts new file mode 100644 index 000000000000..a2b1d329aa0a --- /dev/null +++ b/src/utils/keyboard/index.ts @@ -0,0 +1,32 @@ +import {Keyboard} from 'react-native'; + +let isVisible = false; + +Keyboard.addListener('keyboardDidHide', () => { + isVisible = false; +}); + +Keyboard.addListener('keyboardDidShow', () => { + isVisible = true; +}); + +const dismiss = (): Promise => { + return new Promise((resolve) => { + if (!isVisible) { + resolve(); + + return; + } + + const subscription = Keyboard.addListener('keyboardDidHide', () => { + resolve(undefined); + subscription.remove(); + }); + + Keyboard.dismiss(); + }); +}; + +const utils = {dismiss}; + +export default utils; diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts new file mode 100644 index 000000000000..de7e89b9d660 --- /dev/null +++ b/src/utils/keyboard/index.website.ts @@ -0,0 +1,46 @@ +import {InteractionManager, Keyboard} from 'react-native'; + +let isVisible = false; + +const isKeyboardInput = (elem: HTMLElement): boolean => { + const inputTypesToIgnore = ['button', 'submit', 'checkbox', 'file', 'image']; + return (elem.tagName === 'INPUT' && !inputTypesToIgnore.includes((elem as HTMLInputElement).type)) || elem.tagName === 'TEXTAREA' || elem.hasAttribute('contenteditable'); +}; + +const handleFocusIn = (event: FocusEvent): void => { + const target = event.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isVisible = true; + } +}; + +const handleFocusOut = (event: FocusEvent): void => { + const target = event.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isVisible = false; + } +}; + +document.addEventListener('focusin', handleFocusIn); +document.addEventListener('focusout', handleFocusOut); + +const dismiss = (): Promise => { + return new Promise((resolve) => { + if (!isVisible) { + resolve(); + return; + } + + Keyboard.dismiss(); + InteractionManager.runAfterInteractions(() => { + isVisible = false; + resolve(); + }); + }); +}; + +const utils = { + dismiss, +}; + +export default utils; From e418c04375475557bab78ce1b93df1acc65a056a Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 7 Dec 2024 00:53:08 +0530 Subject: [PATCH 13/62] fix: Add an Invite button to the workspace profile page. Signed-off-by: krishna2323 --- src/pages/workspace/WorkspaceProfilePage.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 4a4862152d53..44c3417ae3f3 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -54,6 +54,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac // When we create a new workspace, the policy prop will be empty on the first render. Therefore, we have to use policyDraft until policy has been set in Onyx. const policy = policyDraft?.id ? policyDraft : policyProp; + const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const outputCurrency = policy?.outputCurrency ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.currency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? ''; const currencySymbol = currencyList?.[outputCurrency]?.symbol ?? ''; const formattedCurrency = !isEmptyObject(policy) && !isEmptyObject(currencyList) ? `${outputCurrency} - ${currencySymbol}` : ''; @@ -269,6 +270,16 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac )} {!readOnly && ( + {isPolicyAdmin && ( +