Skip to content

53814 fix confirmation button jump #55494

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

Merged
merged 4 commits into from
Jan 22, 2025
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
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import CustomStatusBarAndBackgroundContextProvider from './components/CustomStat
import ErrorBoundary from './components/ErrorBoundary';
import HTMLEngineProvider from './components/HTMLEngineProvider';
import InitialURLContextProvider from './components/InitialURLContextProvider';
import {InputBlurContextProvider} from './components/InputBlurContext';
import KeyboardProvider from './components/KeyboardProvider';
import {LocaleContextProvider} from './components/LocaleContextProvider';
import OnyxProvider from './components/OnyxProvider';
Expand Down Expand Up @@ -98,6 +99,7 @@ function App({url}: AppProps) {
KeyboardProvider,
SearchRouterContextProvider,
ProductTrainingContextProvider,
InputBlurContextProvider,
]}
>
<CustomStatusBarAndBackground />
Expand Down
33 changes: 21 additions & 12 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import {useFocusEffect} from '@react-navigation/native';
import lodashIsEqual from 'lodash/isEqual';
import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'react';
import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {InteractionManager} from 'react-native';
import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import {useInputBlurContext} from '@components/InputBlurContext';
import useDebounceNonReactive from '@hooks/useDebounceNonReactive';
import useLocalize from '@hooks/useLocalize';
import * as ValidationUtils from '@libs/ValidationUtils';
import {isSafari} from '@libs/Browser';
import {prepareValues} from '@libs/ValidationUtils';
import Visibility from '@libs/Visibility';
import * as FormActions from '@userActions/FormActions';
import {clearErrorFields, clearErrors, setDraftValues, setErrors as setFormErrors} from '@userActions/FormActions';
import CONST from '@src/CONST';
import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -95,15 +98,16 @@ function FormProvider(
const [inputValues, setInputValues] = useState<Form>(() => ({...draftValues}));
const [errors, setErrors] = useState<GenericFormInputErrors>({});
const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]);
const {setIsBlurred} = useInputBlurContext();

const onValidate = useCallback(
(values: FormOnyxValues, shouldClearServerError = true) => {
const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(values) : values;
const trimmedStringValues = shouldTrimValues ? prepareValues(values) : values;

if (shouldClearServerError) {
FormActions.clearErrors(formID);
clearErrors(formID);
}
FormActions.clearErrorFields(formID);
clearErrorFields(formID);

const validateErrors: GenericFormInputErrors = validate?.(trimmedStringValues) ?? {};

Expand Down Expand Up @@ -168,7 +172,7 @@ function FormProvider(
}

// Prepare validation values
const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(inputValues) : inputValues;
const trimmedStringValues = shouldTrimValues ? prepareValues(inputValues) : inputValues;

// Validate in order to make sure the correct error translations are displayed,
// making sure to not clear server errors if they exist
Expand All @@ -194,7 +198,7 @@ function FormProvider(
}

// Prepare values before submitting
const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(inputValues) : inputValues;
const trimmedStringValues = shouldTrimValues ? prepareValues(inputValues) : inputValues;

// Touches all form inputs, so we can validate the entire form
Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true));
Expand Down Expand Up @@ -246,16 +250,16 @@ function FormProvider(
);

const resetErrors = useCallback(() => {
FormActions.clearErrors(formID);
FormActions.clearErrorFields(formID);
clearErrors(formID);
clearErrorFields(formID);
setErrors({});
}, [formID]);

const resetFormFieldError = useCallback(
(inputID: keyof Form) => {
const newErrors = {...errors};
delete newErrors[inputID];
FormActions.setErrors(formID, newErrors as Errors);
setFormErrors(formID, newErrors as Errors);
setErrors(newErrors);
},
[errors, formID],
Expand Down Expand Up @@ -371,6 +375,11 @@ function FormProvider(
}, VALIDATE_DELAY);
}
inputProps.onBlur?.(event);
if (isSafari()) {
InteractionManager.runAfterInteractions(() => {
setIsBlurred(true);
});
}
},
onInputChange: (value, key) => {
const inputKey = key ?? inputID;
Expand All @@ -387,13 +396,13 @@ function FormProvider(
});

if (inputProps.shouldSaveDraft && !formID.includes('Draft')) {
FormActions.setDraftValues(formID, {[inputKey]: value});
setDraftValues(formID, {[inputKey]: value});
}
inputProps.onValueChange?.(value, inputKey);
},
};
},
[draftValues, inputValues, formState?.errorFields, errors, submit, setTouchedInput, shouldValidateOnBlur, onValidate, hasServerError, formID, shouldValidateOnChange],
[draftValues, inputValues, formState?.errorFields, errors, submit, setTouchedInput, shouldValidateOnBlur, onValidate, hasServerError, setIsBlurred, formID, shouldValidateOnChange],
);
const value = useMemo(() => ({registerInput}), [registerInput]);

Expand Down
32 changes: 32 additions & 0 deletions src/components/InputBlurContext/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, {useContext, useMemo, useState} from 'react';
import type ChildrenProps from '@src/types/utils/ChildrenProps';

type InputBlurContextType = {
isBlurred: boolean; // Boolean state to track blur
setIsBlurred: React.Dispatch<React.SetStateAction<boolean>>; // Function to update the state
};

const InputBlurContext = React.createContext<InputBlurContextType>({
isBlurred: true,
setIsBlurred: () => {},
});

function InputBlurContextProvider({children}: ChildrenProps) {
const [isBlurred, setIsBlurred] = useState<boolean>(false);

const contextValue = useMemo(
() => ({
isBlurred,
setIsBlurred,
}),
[isBlurred],
);

return <InputBlurContext.Provider value={contextValue}>{children}</InputBlurContext.Provider>;
}

function useInputBlurContext() {
return useContext(InputBlurContext);
}

export {InputBlurContext, useInputBlurContext, InputBlurContextProvider};
34 changes: 29 additions & 5 deletions src/components/ScreenWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useTackInputFocus from '@hooks/useTackInputFocus';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Browser from '@libs/Browser';
import {isMobile, isMobileWebKit, isSafari} from '@libs/Browser';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types';
import addViewportResizeListener from '@libs/VisualViewport';
import toggleTestToolsModal from '@userActions/TestTool';
import CONST from '@src/CONST';
import CustomDevMenu from './CustomDevMenu';
import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen';
import type FocusTrapForScreenProps from './FocusTrap/FocusTrapForScreen/FocusTrapProps';
import HeaderGap from './HeaderGap';
import ImportedStateIndicator from './ImportedStateIndicator';
import {useInputBlurContext} from './InputBlurContext';
import KeyboardAvoidingView from './KeyboardAvoidingView';
import ModalContext from './Modal/ModalContext';
import OfflineIndicator from './OfflineIndicator';
Expand Down Expand Up @@ -159,12 +161,13 @@ function ScreenWrapper(
const {isDevelopment} = useEnvironment();
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined;
const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined;
const minHeight = shouldEnableMinHeight && !isSafari() ? initialHeight : undefined;

const route = useRoute();
const shouldReturnToOldDot = useMemo(() => {
return !!route?.params && 'singleNewDotEntry' in route.params && route.params.singleNewDotEntry === 'true';
}, [route?.params]);
const {isBlurred, setIsBlurred} = useInputBlurContext();

UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => {
NativeModules.HybridAppModule?.closeReactNativeApp(false, false);
Expand All @@ -181,14 +184,35 @@ function ScreenWrapper(
PanResponder.create({
onMoveShouldSetPanResponderCapture: (_e, gestureState) => {
const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy);
const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && Browser.isMobile();
const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && isMobile();

return isHorizontalSwipe && shouldDismissKeyboard;
},
onPanResponderGrant: Keyboard.dismiss,
}),
).current;

useEffect(() => {
/**
* Handler to manage viewport resize events specific to Safari.
* Disables the blur state when Safari is detected.
*/
const handleViewportResize = () => {
if (!isSafari()) {
return; // Exit early if not Safari
}
setIsBlurred(false); // Disable blur state for Safari
};

// Add the viewport resize listener
const removeResizeListener = addViewportResizeListener(handleViewportResize);

// Cleanup function to remove the listener
return () => {
removeResizeListener();
};
}, [setIsBlurred]);

useEffect(() => {
// On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout
const timeout = setTimeout(() => {
Expand Down Expand Up @@ -249,7 +273,7 @@ function ScreenWrapper(
paddingStyle.paddingBottom = unmodifiedPaddings.bottom;
}

const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit());
const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && isMobileWebKit());
const contextValue = useMemo(
() => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied: includeSafeAreaPaddingBottom}),
[didScreenTransitionEnd, includeSafeAreaPaddingBottom, isSafeAreaTopPaddingApplied],
Expand All @@ -271,7 +295,7 @@ function ScreenWrapper(
{...keyboardDismissPanResponder.panHandlers}
>
<KeyboardAvoidingView
style={[styles.w100, styles.h100, {maxHeight}, isAvoidingViewportScroll ? [styles.overflowAuto, styles.overscrollBehaviorContain] : {}]}
style={[styles.w100, styles.h100, !isBlurred ? {maxHeight} : undefined, isAvoidingViewportScroll ? [styles.overflowAuto, styles.overscrollBehaviorContain] : {}]}
behavior={keyboardAvoidingViewBehavior}
enabled={shouldEnableKeyboardAvoidingView}
>
Expand Down
Loading