11import React , {
22 forwardRef ,
33 useCallback ,
4+ useEffect ,
45 useImperativeHandle ,
6+ useLayoutEffect ,
57 useMemo ,
68 useRef ,
79 useState ,
8- useLayoutEffect ,
9- useEffect ,
1010 type ComponentRef ,
1111} from 'react' ;
1212import {
1313 Animated ,
14- View ,
14+ Keyboard ,
1515 PanResponder ,
16+ Platform ,
1617 StyleSheet ,
17- type LayoutChangeEvent ,
1818 useWindowDimensions ,
19- Keyboard ,
20- Platform ,
19+ View ,
20+ type LayoutChangeEvent ,
2121} from 'react-native' ;
2222import {
2323 DEFAULT_ANIMATION ,
@@ -26,23 +26,23 @@ import {
2626 DEFAULT_HEIGHT ,
2727 DEFAULT_OPEN_ANIMATION_DURATION ,
2828} from '../../constant' ;
29- import DefaultHandleBar from '../defaultHandleBar' ;
30- import Container from '../container' ;
31- import normalizeHeight from '../../utils/normalizeHeight' ;
32- import convertHeight from '../../utils/convertHeight' ;
33- import useHandleKeyboardEvents from '../../hooks/useHandleKeyboardEvents' ;
3429import useAnimatedValue from '../../hooks/useAnimatedValue' ;
30+ import useHandleAndroidBackButtonClose from '../../hooks/useHandleAndroidBackButtonClose' ;
31+ import useHandleKeyboardEvents from '../../hooks/useHandleKeyboardEvents' ;
32+ import convertHeight from '../../utils/convertHeight' ;
33+ import normalizeHeight from '../../utils/normalizeHeight' ;
34+ import separatePaddingStyles from '../../utils/separatePaddingStyles' ;
3535import Backdrop from '../backdrop' ;
36+ import Container from '../container' ;
37+ import DefaultHandleBar from '../defaultHandleBar' ;
3638import {
37- type BottomSheetProps ,
38- type ToValue ,
3939 ANIMATIONS ,
40- type BottomSheetMethods ,
4140 CUSTOM_BACKDROP_POSITIONS ,
4241 type BOTTOMSHEET ,
42+ type BottomSheetMethods ,
43+ type BottomSheetProps ,
44+ type ToValue ,
4345} from './types.d' ;
44- import useHandleAndroidBackButtonClose from '../../hooks/useHandleAndroidBackButtonClose' ;
45- import separatePaddingStyles from '../../utils/separatePaddingStyles' ;
4646
4747/**
4848 * Main bottom sheet component
@@ -106,8 +106,10 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
106106
107107 const contentWrapperRef = useRef < ComponentRef < typeof Animated . View > > ( null ) ;
108108
109- /** cached _nativeTag property of content container */
110- const cachedContentWrapperNativeTag = useRef < number | undefined > ( undefined ) ;
109+ /** cached unique identifier of content container */
110+ const cachedContentWrapperId = useRef <
111+ { field : string ; value : unknown } | undefined
112+ > ( undefined ) ;
111113
112114 // here we separate all padding that may be applied via contentContainerStyle prop,
113115 // these paddings will be applied to the `View` diretly wrapping `ChildNodes` in content container.
@@ -120,20 +122,27 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
120122 ) ;
121123
122124 // Animation utility
123- const Animators = useMemo (
124- ( ) => ( {
125- _slideEasingFn ( value : number ) {
126- return value === 1 ? 1 : 1 - Math . pow ( 2 , - 10 * value ) ;
127- } ,
128- _springEasingFn ( value : number ) {
129- const c4 = ( 2 * Math . PI ) / 2.5 ;
130- return value === 0
131- ? 0
132- : value === 1
133- ? 1
134- : Math . pow ( 2 , - 9 * value ) * Math . sin ( ( value * 4.5 - 0.75 ) * c4 ) +
135- 1 ;
136- } ,
125+ const Animators = useMemo ( ( ) => {
126+ const _slideEasingFn = ( value : number ) => {
127+ return value === 1 ? 1 : 1 - Math . pow ( 2 , - 10 * value ) ;
128+ } ;
129+ const _springEasingFn = ( value : number ) => {
130+ const decay = 9 ;
131+ const multiplier = 4.5 ;
132+ const divisor = 2.3 ;
133+
134+ const c4 = ( 2 * Math . PI ) / divisor ;
135+
136+ return value === 0
137+ ? 0
138+ : value === 1
139+ ? 1
140+ : Math . pow ( 2 , - decay * value ) *
141+ Math . sin ( ( value * multiplier - 0.75 ) * c4 ) +
142+ 1 ;
143+ } ;
144+
145+ return {
137146 animateContainerHeight ( toValue : ToValue , duration : number = 0 ) {
138147 return Animated . timing ( _animatedContainerHeight , {
139148 toValue : toValue ,
@@ -162,19 +171,18 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
162171 customEasingFunction && typeof customEasingFunction === 'function'
163172 ? customEasingFunction
164173 : animationType === ANIMATIONS . SLIDE
165- ? this . _slideEasingFn
166- : this . _springEasingFn ,
174+ ? _slideEasingFn
175+ : _springEasingFn ,
167176 } ) ;
168177 } ,
169- } ) ,
170- [
171- animationType ,
172- customEasingFunction ,
173- _animatedContainerHeight ,
174- _animatedBackdropMaskOpacity ,
175- _animatedHeight ,
176- ]
177- ) ;
178+ } ;
179+ } , [
180+ animationType ,
181+ customEasingFunction ,
182+ _animatedContainerHeight ,
183+ _animatedBackdropMaskOpacity ,
184+ _animatedHeight ,
185+ ] ) ;
178186
179187 const interpolatedOpacity = useMemo (
180188 ( ) =>
@@ -241,21 +249,16 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
241249 if ( view === 'handlebar' && disableDragHandlePanning ) return null ;
242250 if ( view === 'contentwrapper' && disableBodyPanning ) return null ;
243251 return PanResponder . create ( {
244- onMoveShouldSetPanResponder : ( evt ) => {
245- /**
246- * `FiberNode._nativeTag` is stable across renders so we use it to determine
247- * whether content container or it's child should respond to touch move gesture.
248- *
249- * The logic is, when content container is laid out, we extract it's _nativeTag property and cache it
250- * So later when a move gesture event occurs within it, we compare the cached _nativeTag with the _nativeTag of
251- * the event target's _nativeTag, if they match, then content container should respond, else its children should.
252- * Also, when the target is the handle bar, we le it handle geture unless panning is disabled through props
253- */
254- return view === 'handlebar'
255- ? true
256- : cachedContentWrapperNativeTag . current ===
257- // @ts -expect-error
258- evt ?. target ?. _nativeTag ;
252+ onMoveShouldSetPanResponderCapture : ( evt ) => {
253+ if ( view === 'handlebar' ) return true ;
254+ const cached = cachedContentWrapperId . current ;
255+ if ( ! cached ) return false ; // this signature alone should fix issue #34
256+ return (
257+ // @ts -expect-error _private field access
258+ cached ?. value === evt ?. target ?. [ cached ?. field ] ||
259+ // @ts -expect-error _private field access
260+ cached ?. value === evt ?. currentTarget ?. [ cached ?. field ]
261+ ) ;
259262 } ,
260263 onPanResponderMove : ( _ , gestureState ) => {
261264 if ( gestureState . dy > 0 ) {
@@ -304,17 +307,65 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
304307 /* eslint-enable react/no-unstable-nested-components, react-native/no-inline-styles */
305308
306309 /**
307- * Extracts and caches the _nativeTag property of ContentWrapper
310+ * Extracts and caches either `_nativeTag` or `__nativeTag` or `__internalInstanceHandle` or `_internalFiberInstanceHandleDEV`
311+ * reference of the `ContentWrapper` component based on which is available. Either will do for
312+ * identifying the content wrapper in PanResponder
308313 */
309- let extractNativeTag = useCallback ( ( { target } : LayoutChangeEvent ) => {
310- const tag =
311- Platform . OS === 'web'
312- ? undefined
313- : // @ts -expect-error
314- target ?. _nativeTag ;
315- if ( ! cachedContentWrapperNativeTag . current )
316- cachedContentWrapperNativeTag . current = tag ;
317- } , [ ] ) ;
314+ const cacheElementReference = useCallback (
315+ ( { currentTarget, nativeEvent } : LayoutChangeEvent ) => {
316+ const fabricInstanceHandleKey = '__internalInstanceHandle' ;
317+ // @ts -expect-error `Fabric` renderer's instance handle reference/pointer
318+ const fabricInstanceHandle = currentTarget ?. [ fabricInstanceHandleKey ] ;
319+
320+ const oldNativeTagKey = '_nativeTag' ;
321+ // @ts -expect-error `Paper` renderer's native tag number
322+ const oldNativeTag = currentTarget ?. [ oldNativeTagKey ] ;
323+
324+ const newNativeTagKey = '__nativeTag' ;
325+ // @ts -expect-error `Fabric` renderer's native tag number
326+ const newNativeTag = currentTarget ?. [ newNativeTagKey ] ;
327+
328+ const paperInstanceHandleKey = '_internalFiberInstanceHandleDEV' ;
329+ // @ts -expect-error `Paper` renderer's instance handle equivalent
330+ const paperInstanceHandle = currentTarget ?. [ paperInstanceHandleKey ] ;
331+
332+ if ( ! cachedContentWrapperId . current ) {
333+ if ( fabricInstanceHandle )
334+ cachedContentWrapperId . current = {
335+ field : fabricInstanceHandleKey ,
336+ value : fabricInstanceHandle ,
337+ } ;
338+ else if ( oldNativeTag )
339+ cachedContentWrapperId . current = {
340+ field : oldNativeTagKey ,
341+ value : oldNativeTag ,
342+ } ;
343+ else if ( newNativeTag )
344+ cachedContentWrapperId . current = {
345+ field : newNativeTagKey ,
346+ value : newNativeTag ,
347+ } ;
348+ else if ( paperInstanceHandle )
349+ cachedContentWrapperId . current = {
350+ field : paperInstanceHandleKey ,
351+ value : paperInstanceHandle ,
352+ } ;
353+ // Check known stable keys for web if none of above exists
354+ else if ( Platform . OS === 'web' ) {
355+ const responderKey = '__reactResponderId' ;
356+ // @ts -expect-error `.target` is untyped
357+ const responderId = nativeEvent ?. target ?. [ responderKey ] ;
358+ if ( responderId ) {
359+ cachedContentWrapperId . current = {
360+ field : responderKey ,
361+ value : responderId ,
362+ } ;
363+ }
364+ } else cachedContentWrapperId . current = undefined ;
365+ }
366+ } ,
367+ [ ]
368+ ) ;
318369
319370 /**
320371 * Expands the bottom sheet.
@@ -341,20 +392,31 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
341392 } ;
342393
343394 const closeBottomSheet = ( ) => {
344- // 1. fade backdrop
345- // 2. if using fade animation, close container, set content wrapper height to 0.
346- // else animate content container height & container height to 0, in sequence
347- Animators . animateBackdropMaskOpacity ( 0 , closeDuration ) . start ( ( anim ) => {
348- if ( anim . finished ) {
349- if ( animationType === ANIMATIONS . FADE ) {
395+ if ( animationType === ANIMATIONS . FADE ) {
396+ // For fade, sheet opacity is tied to the backdrop, so we wait for the
397+ // backdrop fade to complete before snapping the container shut.
398+ Animators . animateBackdropMaskOpacity ( 0 , closeDuration ) . start ( ( anim ) => {
399+ if ( anim . finished ) {
350400 Animators . animateContainerHeight ( 0 ) . start ( ) ;
351401 _animatedHeight . setValue ( 0 ) ;
352- } else {
353- Animators . animateHeight ( 0 , closeDuration ) . start ( ) ;
354- Animators . animateContainerHeight ( 0 ) . start ( ) ;
355402 }
356- }
357- } ) ;
403+ } ) ;
404+ } else if ( animationType === ANIMATIONS . SLIDE ) {
405+ // Run backdrop fade and height slide-out in parallel so flick-to-close
406+ // doesn't pause mid-flight waiting for the (faster) backdrop fade.
407+ // Snap the outer container to 0 only after the sheet has
408+ // finished sliding out so the slide animation isn't clipped.
409+ Animators . animateBackdropMaskOpacity ( 0 , closeDuration ) . start ( ) ;
410+ Animators . animateHeight ( 0 , closeDuration ) . start ( ( anim ) => {
411+ if ( anim . finished ) Animators . animateContainerHeight ( 0 ) . start ( ) ;
412+ } ) ;
413+ } else {
414+ Animators . animateBackdropMaskOpacity ( 0 , closeDuration ) . start ( ) ;
415+ // `animateHeight` and `animateContainerHeight` below need to run in parallel
416+ // else there might be a noticeable flicker of sheet content
417+ Animators . animateHeight ( 0 , closeDuration ) . start ( ) ;
418+ Animators . animateContainerHeight ( 0 ) . start ( ) ;
419+ }
358420 setSheetOpen ( false ) ;
359421 keyboardHandler ?. removeKeyboardListeners ( ) ;
360422 Keyboard . dismiss ( ) ;
@@ -484,11 +546,21 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
484546 < Animated . View
485547 ref = { contentWrapperRef }
486548 key = { 'BottomSheetContentContainer' }
487- onLayout = { extractNativeTag }
549+ onLayout = { cacheElementReference }
488550 /* Merge external and internal styles carefully and orderly */
489551 style = { [
490552 ! modal ? materialStyles . contentContainerShadow : false ,
491553 materialStyles . contentContainer ,
554+ // Apply default top-corner radii only when the user hasn't
555+ // supplied a `borderRadius` shorthand since RN's render layer keeps
556+ // individual corner properties over the shorthand, so leaving
557+ // them in would silently override the user's value.
558+ ! (
559+ sepStyles ?. otherStyles &&
560+ 'borderRadius' in sepStyles . otherStyles
561+ )
562+ ? materialStyles . contentContainerTopRadius
563+ : false ,
492564 // we apply styles other than padding here
493565 sepStyles ?. otherStyles ,
494566 {
@@ -502,8 +574,11 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
502574 < PolymorphicHandleBar />
503575
504576 < View
505- // we apply padding styles here to not affect drag handle above
506- style = { sepStyles ?. paddingStyles }
577+ // we apply padding styles here to not affect drag handle above.
578+ // `flex: 1` lets this fill the remaining space below the drag
579+ // handle so children sized with `flex` or percentage heights
580+ // render against the actual available area (issue #36).
581+ style = { [ materialStyles . contentBody , sepStyles ?. paddingStyles ] }
507582 >
508583 { ChildNodes }
509584 </ View >
@@ -522,9 +597,14 @@ const materialStyles = StyleSheet.create({
522597 backgroundColor : '#F7F2FA' ,
523598 width : '100%' ,
524599 overflow : 'hidden' ,
600+ } ,
601+ contentContainerTopRadius : {
525602 borderTopLeftRadius : 28 ,
526603 borderTopRightRadius : 28 ,
527604 } ,
605+ contentBody : {
606+ flex : 1 ,
607+ } ,
528608 contentContainerShadow :
529609 Platform . OS === 'android'
530610 ? {
0 commit comments