@@ -6,14 +6,21 @@ import {
66} from '@react-navigation/native'
77import * as React from 'react'
88import { useEffect , useMemo , useState } from 'react'
9- import { Keyboard , StyleSheet , View , type ViewStyle } from 'react-native'
9+ import {
10+ Keyboard ,
11+ Platform ,
12+ StyleSheet ,
13+ View ,
14+ type ViewStyle
15+ } from 'react-native'
1016import {
1117 useKeyboardHandler ,
1218 useReanimatedKeyboardAnimation
1319} from 'react-native-keyboard-controller'
1420import Reanimated , {
1521 useAnimatedReaction ,
16- useAnimatedStyle
22+ useAnimatedStyle ,
23+ useSharedValue
1724} from 'react-native-reanimated'
1825import {
1926 type EdgeInsets ,
@@ -118,6 +125,13 @@ interface SceneWrapperProps {
118125
119126 // True to make the scene scrolling (if avoidKeyboard is false):
120127 scroll ?: boolean
128+
129+ // Optional KeyboardAccessoryView rendered as a sibling to the SceneWrapper content:
130+ kavProps ?: {
131+ children : React . ReactNode
132+ alwaysVisible ?: boolean
133+ contentContainerStyle ?: ViewStyle
134+ }
121135}
122136
123137/**
@@ -147,12 +161,14 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement {
147161 hasTabs = false ,
148162 padding = 0 ,
149163 renderFooter,
150- scroll = false
164+ scroll = false ,
165+ kavProps
151166 } = props
152167
153168 const notificationHeight = useSelector ( state => state . ui . notificationHeight )
154169
155170 const navigation = useNavigation < NavigationBase > ( )
171+ const isIos = Platform . OS === 'ios'
156172
157173 // We need to track this state in the JS thread because insets are not shared values
158174 const [ isKeyboardOpen , setIsKeyboardOpen ] = useState ( false )
@@ -163,6 +179,48 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement {
163179 }
164180 } )
165181
182+ // Local keyboard visible state for KAV parity (iOS willShow vs Android didShow):
183+ const [ isKeyboardVisibleFull , setKeyboardVisibleKav ] = useState ( false )
184+ useEffect ( ( ) => {
185+ const keyboardDidShowListener = Keyboard . addListener (
186+ isIos ? 'keyboardWillShow' : 'keyboardDidShow' ,
187+ ( ) => {
188+ setKeyboardVisibleKav ( true )
189+ }
190+ )
191+ const keyboardDidHideListener = Keyboard . addListener (
192+ isIos ? 'keyboardWillHide' : 'keyboardDidHide' ,
193+ ( ) => {
194+ setKeyboardVisibleKav ( false )
195+ }
196+ )
197+ return ( ) => {
198+ keyboardDidShowListener . remove ( )
199+ keyboardDidHideListener . remove ( )
200+ }
201+ } , [ isIos ] )
202+
203+ // Track closing/opening state explicitly for animation direction:
204+ const isClosingSv = useSharedValue ( false )
205+ useEffect ( ( ) => {
206+ const showListener = Keyboard . addListener (
207+ isIos ? 'keyboardWillShow' : 'keyboardDidShow' ,
208+ ( ) => {
209+ isClosingSv . value = false
210+ }
211+ )
212+ const hideListener = Keyboard . addListener (
213+ isIos ? 'keyboardWillHide' : 'keyboardDidHide' ,
214+ ( ) => {
215+ isClosingSv . value = true
216+ }
217+ )
218+ return ( ) => {
219+ showListener . remove ( )
220+ hideListener . remove ( )
221+ }
222+ } , [ isIos , isClosingSv ] )
223+
166224 // Reset the footer ratio when focused
167225 // We can do this because multiple calls to resetFooterRatio isn't costly
168226 // because it just sets snapTo SharedValue to `1`
@@ -233,6 +291,22 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement {
233291 [ insets . top , insets . right , insets . bottom , insets . left ]
234292 )
235293
294+ // Collapsed bottom inset that ignores keyboard-open state (used to clamp during close):
295+ const collapsedInsetBottom = useMemo (
296+ ( ) =>
297+ safeAreaInsets . bottom +
298+ ( hasNotifications ? notificationHeight : 0 ) +
299+ ( hasTabs ? MAX_TAB_BAR_HEIGHT : 0 ) +
300+ footerHeight ,
301+ [
302+ footerHeight ,
303+ hasNotifications ,
304+ hasTabs ,
305+ notificationHeight ,
306+ safeAreaInsets . bottom
307+ ]
308+ )
309+
236310 // This is a convenient styles object which may be applied to scene container
237311 // components to offset the inset styles applied to the SceneWrapper.
238312 const undoInsetStyle : UndoInsetStyle = useMemo (
@@ -267,6 +341,54 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement {
267341 return children
268342 } , [ children , sceneWrapperInfo ] )
269343
344+ // Build KAV element (custom, no third-party lib):
345+ const alwaysVisibleKav = kavProps ?. alwaysVisible === true
346+ const kavBaseStyle = useMemo (
347+ ( ) => ( {
348+ position : 'absolute' as const ,
349+ left : 0 ,
350+ right : 0 ,
351+ bottom : 0 ,
352+ backgroundColor : 'transparent'
353+ } ) ,
354+ [ ]
355+ )
356+ const insetBottomSv = useSharedValue ( insets . bottom )
357+ const collapsedInsetSv = useSharedValue ( collapsedInsetBottom )
358+ useEffect ( ( ) => {
359+ insetBottomSv . value = insets . bottom
360+ collapsedInsetSv . value = collapsedInsetBottom
361+ } , [ collapsedInsetBottom , insets . bottom , collapsedInsetSv , insetBottomSv ] )
362+ const kavAnimatedStyle = useAnimatedStyle ( ( ) => {
363+ // keyboardHeightDiff.value is negative when open; invert to get height
364+ const keyboardHeight =
365+ keyboardHeightDiff . value < 0 ? - keyboardHeightDiff . value : 0
366+ const isClosing = isClosingSv . value
367+
368+ let bottom : number
369+ if ( keyboardHeight > 0 ) {
370+ // While opening, ignore insets to hug keyboard.
371+ // While closing, never dip below the insets (avoid flicker under tab bar).
372+ bottom = isClosing
373+ ? Math . max ( keyboardHeight , collapsedInsetSv . value )
374+ : keyboardHeight
375+ } else {
376+ // Settled closed: rest above insets.
377+ bottom = collapsedInsetSv . value
378+ }
379+
380+ return { bottom }
381+ } )
382+ const shouldShowKav =
383+ kavProps != null && ( alwaysVisibleKav || isKeyboardVisibleFull )
384+ const kavElement = ! shouldShowKav ? null : (
385+ < Reanimated . View
386+ style = { [ kavBaseStyle , kavAnimatedStyle , kavProps ?. contentContainerStyle ] }
387+ >
388+ { kavProps ?. children }
389+ </ Reanimated . View >
390+ )
391+
270392 if ( scroll ) {
271393 return (
272394 < >
@@ -300,6 +422,7 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement {
300422 navigation = { navigation }
301423 />
302424 ) : null }
425+ { kavElement }
303426 < FloatingNavFixer navigation = { navigation } />
304427 </ >
305428 )
@@ -342,6 +465,7 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement {
342465 navigation = { navigation }
343466 />
344467 ) : null }
468+ { kavElement }
345469 < FloatingNavFixer navigation = { navigation } />
346470 </ >
347471 )
@@ -377,6 +501,7 @@ function SceneWrapperComponent(props: SceneWrapperProps): React.ReactElement {
377501 navigation = { navigation }
378502 />
379503 ) : null }
504+ { kavElement }
380505 < FloatingNavFixer navigation = { navigation } />
381506 </ >
382507 )
0 commit comments