Skip to content

Commit

Permalink
Merge pull request #55494 from huult/53814-fix-confirmation-button-jump
Browse files Browse the repository at this point in the history
53814 fix confirmation button jump
  • Loading branch information
Gonals authored Jan 22, 2025
2 parents e716659 + 7784586 commit f8f54cf
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 17 deletions.
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

0 comments on commit f8f54cf

Please sign in to comment.