@@ -15,6 +15,7 @@ import {
1515 findNodeHandle ,
1616 Keyboard ,
1717 KeyboardAvoidingView ,
18+ LayoutAnimation ,
1819 Platform ,
1920 ScrollView ,
2021 StyleSheet ,
@@ -24,47 +25,6 @@ import {
2425 type TextInput ,
2526} from 'react-native' ;
2627
27- let AnimatedReanimatedView : any ;
28- let useAnimatedKeyboard : any ;
29- let useAnimatedStyle : any ;
30- let isReanimatedAvailable = false ;
31-
32- try {
33- const rea = require ( 'react-native-reanimated' ) ;
34- useAnimatedKeyboard = rea . useAnimatedKeyboard ;
35- useAnimatedStyle = rea . useAnimatedStyle ;
36- const createAnimatedComponent = rea . createAnimatedComponent ;
37- // createAnimatedComponent(View) is the canonical way to get an Animated View
38- // that actually drives Reanimated animated styles on the UI thread.
39- if ( useAnimatedKeyboard && useAnimatedStyle && createAnimatedComponent ) {
40- AnimatedReanimatedView = createAnimatedComponent ( View ) ;
41- isReanimatedAvailable = true ;
42- }
43- } catch ( e ) {
44- // gracefully degrade to Animated.timing if not found
45- }
46-
47- // ReanimatedSpacer — used only for the scroll content spacer (not the accessory)
48- const ReanimatedSpacer = ( {
49- avoidKeyboard,
50- offset,
51- extraPad,
52- } : {
53- avoidKeyboard : boolean ;
54- offset : number ;
55- extraPad : number ;
56- } ) => {
57- const keyboard = useAnimatedKeyboard ( ) ;
58- const animatedStyle = useAnimatedStyle ( ( ) => {
59- const kHeight = avoidKeyboard ? keyboard . height . value : 0 ;
60- const finalHeight = kHeight > 0 ? Math . max ( kHeight + offset , 0 ) : 0 ;
61- return { height : finalHeight + extraPad } ;
62- } , [ avoidKeyboard , offset , extraPad ] ) ;
63-
64- const AnimatedView = AnimatedReanimatedView ;
65- return < AnimatedView style = { animatedStyle } /> ;
66- } ;
67-
6828// NOTE: ReanimatedAccessory has been intentionally removed.
6929// The keyboard accessory view is now animated with a dedicated Animated.timing
7030// effect (see inside FieldForm) that uses the keyboard event's exact duration
@@ -128,11 +88,14 @@ export const FieldForm = forwardRef<FieldFormHandle, FieldFormProps>((props, ref
12888 keyboardAccessoryView,
12989 keyboardAccessoryViewMode = 'always' ,
13090 scrollViewRef : scrollViewRefProp ,
91+ resetScrollOnKeyboardHide = false ,
13192 } = props ;
13293
13394 const fieldsRef = useRef < { ref : React . MutableRefObject < TextInput | null > ; skip ?: boolean } [ ] > ( [ ] ) ;
13495 const internalScrollRef = useRef < ScrollView | null > ( null ) ;
13596 const [ accessoryHeight , setAccessoryHeight ] = React . useState ( 0 ) ;
97+ /** Drives bottom padding for accessory when mode is `whenKeyboardOpen` (clears when keyboard closes). */
98+ const [ keyboardOpen , setKeyboardOpen ] = React . useState ( false ) ;
13699
137100 // Ref to the outermost wrapper View — used to measure how far the container
138101 // bottom sits above the real screen bottom (e.g. tab bar height).
@@ -149,12 +112,18 @@ export const FieldForm = forwardRef<FieldFormHandle, FieldFormProps>((props, ref
149112
150113 const setMergedScrollRef = useCallback (
151114 ( node : ScrollView | null ) => {
152- internalScrollRef . current = node ;
115+ const next = node ?? null ;
116+ internalScrollRef . current = next ;
153117 if ( ! scrollViewRefProp ) return ;
154- if ( typeof scrollViewRefProp === 'function' ) {
155- scrollViewRefProp ( node ) ;
156- } else {
157- ( scrollViewRefProp as React . MutableRefObject < ScrollView | null > ) . current = node ;
118+ try {
119+ if ( typeof scrollViewRefProp === 'function' ) {
120+ scrollViewRefProp ( next ) ;
121+ } else {
122+ ( scrollViewRefProp as React . MutableRefObject < ScrollView | null > ) . current =
123+ next ;
124+ }
125+ } catch {
126+ // Parent passed a read-only / frozen ref (e.g. some RN + Reanimated paths).
158127 }
159128 } ,
160129 [ scrollViewRefProp ] ,
@@ -383,30 +352,82 @@ export const FieldForm = forwardRef<FieldFormHandle, FieldFormProps>((props, ref
383352 keyboardHideEvent ?? ( Platform . OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide' ) ;
384353
385354 const showSub = Keyboard . addListener ( showEvent , e => {
355+ setKeyboardOpen ( true ) ;
386356 const h = e . endCoordinates . height ;
387357 const offset = resolveKeyboardVerticalOffset ( keyboardVerticalOffset ) ;
388358 const finalHeight = Math . max ( h + offset , 0 ) ;
389359
390- // Only needed when Reanimated is NOT available (content spacer fallback)
391- if ( ! isReanimatedAvailable && avoidKeyboard ) {
392- Animated . timing ( animatedMargin , {
393- toValue : finalHeight ,
394- duration : e . duration || 250 ,
395- easing : Easing . out ( Easing . poly ( 4 ) ) ,
396- useNativeDriver : false ,
397- } ) . start ( ) ;
360+ // Scroll bottom spacer (non‑iOS, or iOS when not using the static spacer below).
361+ // Avoid useAnimatedKeyboard here — it can throw on New Architecture (frozen ref).
362+ if ( avoidKeyboard ) {
363+ if ( Platform . OS === 'ios' ) {
364+ // Spacer height is fixed on iOS; animating this value still costs JS frames.
365+ animatedMargin . setValue ( 0 ) ;
366+ } else {
367+ Animated . timing ( animatedMargin , {
368+ toValue : finalHeight ,
369+ duration : e . duration || 250 ,
370+ easing : Easing . out ( Easing . poly ( 4 ) ) ,
371+ useNativeDriver : false ,
372+ } ) . start ( ) ;
373+ }
398374 }
399375 onKeyboardShow ?.( { height : h , event : e } ) ;
400376 } ) ;
401377
402378 const hideSub = Keyboard . addListener ( hideEvent , e => {
403- if ( ! isReanimatedAvailable && avoidKeyboard ) {
404- Animated . timing ( animatedMargin , {
405- toValue : 0 ,
406- duration : e ?. duration || 250 ,
407- easing : Easing . out ( Easing . poly ( 4 ) ) ,
408- useNativeDriver : false ,
409- } ) . start ( ) ;
379+ const hideDuration = e ?. duration || 250 ;
380+ const hideEasing =
381+ Platform . OS === 'ios'
382+ ? Easing . bezier ( 0.42 , 0 , 1 , 1 )
383+ : Easing . out ( Easing . cubic ) ;
384+
385+ const deferAccessoryPadClear =
386+ Boolean ( keyboardAccessoryView ) &&
387+ keyboardAccessoryViewMode === 'whenKeyboardOpen' ;
388+
389+ const applyKeyboardClosedLayout = ( ) => {
390+ if ( Platform . OS === 'ios' && deferAccessoryPadClear ) {
391+ LayoutAnimation . configureNext ( {
392+ ...LayoutAnimation . Presets . easeInEaseOut ,
393+ duration : Math . min ( hideDuration , 280 ) ,
394+ } ) ;
395+ }
396+ setKeyboardOpen ( false ) ;
397+ } ;
398+
399+ if ( resetScrollOnKeyboardHide && scrollable ) {
400+ const scroll = internalScrollRef . current ;
401+ if ( scroll ) {
402+ // Same frame as keyboard hide — avoids an extra rAF of latency.
403+ scroll . scrollTo ( { x : 0 , y : 0 , animated : false } ) ;
404+ }
405+ }
406+
407+ if ( ! deferAccessoryPadClear ) {
408+ applyKeyboardClosedLayout ( ) ;
409+ }
410+
411+ if ( avoidKeyboard ) {
412+ if ( Platform . OS === 'ios' ) {
413+ animatedMargin . setValue ( 0 ) ;
414+ if ( deferAccessoryPadClear ) {
415+ applyKeyboardClosedLayout ( ) ;
416+ }
417+ } else {
418+ Animated . timing ( animatedMargin , {
419+ toValue : 0 ,
420+ duration : hideDuration ,
421+ easing : hideEasing ,
422+ useNativeDriver : false ,
423+ } ) . start ( ( ) => {
424+ if ( deferAccessoryPadClear ) {
425+ applyKeyboardClosedLayout ( ) ;
426+ }
427+ } ) ;
428+ }
429+ } else if ( deferAccessoryPadClear ) {
430+ applyKeyboardClosedLayout ( ) ;
410431 }
411432 onKeyboardHide ?.( ) ;
412433 } ) ;
@@ -422,6 +443,10 @@ export const FieldForm = forwardRef<FieldFormHandle, FieldFormProps>((props, ref
422443 keyboardHideEvent ,
423444 onKeyboardShow ,
424445 onKeyboardHide ,
446+ resetScrollOnKeyboardHide ,
447+ scrollable ,
448+ keyboardAccessoryView ,
449+ keyboardAccessoryViewMode ,
425450 ] ) ;
426451
427452 const ctx = useMemo (
@@ -466,11 +491,17 @@ export const FieldForm = forwardRef<FieldFormHandle, FieldFormProps>((props, ref
466491 ...kavRest
467492 } = keyboardAvoidingViewProps ?? { } ;
468493
494+ const useAccessoryBottomPad =
495+ Boolean ( keyboardAccessoryView ) &&
496+ ( keyboardAccessoryViewMode === 'always' || keyboardOpen ) ;
497+
469498 const mergedContentContainerStyle = [
470499 applyDefaultScrollContentFlexGrow ? styles . scrollContent : null ,
471500 contentContainerStyle ,
472501 scrollContentFromProps ,
473- keyboardAccessoryView ? { paddingBottom : accessoryHeight } : null ,
502+ useAccessoryBottomPad && accessoryHeight > 0
503+ ? { paddingBottom : accessoryHeight }
504+ : null ,
474505 ] ;
475506
476507 const kavBehavior =
@@ -494,14 +525,15 @@ export const FieldForm = forwardRef<FieldFormHandle, FieldFormProps>((props, ref
494525 { children }
495526 { Platform . OS === 'ios' && avoidKeyboard ? (
496527 < View style = { { height : extraScrollPadding } } />
497- ) : isReanimatedAvailable ? (
498- < ReanimatedSpacer
499- avoidKeyboard = { avoidKeyboard }
500- offset = { kavOffset }
501- extraPad = { typeof extraScrollPadding === 'number' ? extraScrollPadding : 0 }
502- />
503528 ) : (
504- < Animated . View style = { { height : Animated . add ( extraScrollPadding , animatedMargin ) } } />
529+ < Animated . View
530+ style = { {
531+ height : Animated . add (
532+ typeof extraScrollPadding === 'number' ? extraScrollPadding : 0 ,
533+ animatedMargin ,
534+ ) ,
535+ } }
536+ />
505537 ) }
506538 </ >
507539 ) ,
0 commit comments