-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
[RNMobile] Bottom-sheet: Add custom header #30291
Changes from 7 commits
cc6efdc
17ae9a4
32e66c2
c93082a
2a70a38
736fab5
e35bff5
c49b007
7b865ad
467e1b2
e398d51
9a89ce2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,15 +2,16 @@ | |
* External dependencies | ||
*/ | ||
import { | ||
Text, | ||
View, | ||
Platform, | ||
PanResponder, | ||
Dimensions, | ||
Keyboard, | ||
StatusBar, | ||
LayoutAnimation, | ||
PanResponder, | ||
Platform, | ||
ScrollView, | ||
StatusBar, | ||
Text, | ||
TouchableHighlight, | ||
View, | ||
} from 'react-native'; | ||
import Modal from 'react-native-modal'; | ||
import SafeArea from 'react-native-safe-area'; | ||
|
@@ -43,6 +44,8 @@ import BottomSheetSubSheet from './sub-sheet'; | |
import NavigationHeader from './navigation-header'; | ||
import { BottomSheetProvider } from './bottom-sheet-context'; | ||
|
||
const DEFAULT_LAYOUT_ANIMATION = LayoutAnimation.Presets.easeInEaseOut; | ||
|
||
class BottomSheet extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
|
@@ -58,6 +61,7 @@ class BottomSheet extends Component { | |
this.setIsFullScreen = this.setIsFullScreen.bind( this ); | ||
|
||
this.onDimensionsChange = this.onDimensionsChange.bind( this ); | ||
this.onHeaderLayout = this.onHeaderLayout.bind( this ); | ||
this.onCloseBottomSheet = this.onCloseBottomSheet.bind( this ); | ||
this.onHandleClosingBottomSheet = this.onHandleClosingBottomSheet.bind( | ||
this | ||
|
@@ -66,15 +70,18 @@ class BottomSheet extends Component { | |
this.onHandleHardwareButtonPress = this.onHandleHardwareButtonPress.bind( | ||
this | ||
); | ||
this.keyboardWillShow = this.keyboardWillShow.bind( this ); | ||
this.keyboardDidHide = this.keyboardDidHide.bind( this ); | ||
this.keyboardShow = this.keyboardShow.bind( this ); | ||
this.keyboardHide = this.keyboardHide.bind( this ); | ||
|
||
this.headerHeight = 0; | ||
this.keyboardHeight = 0; | ||
this.lastLayoutAnimation = null; | ||
|
||
this.state = { | ||
safeAreaBottomInset: 0, | ||
safeAreaTopInset: 0, | ||
bounces: false, | ||
maxHeight: 0, | ||
keyboardHeight: 0, | ||
scrollEnabled: true, | ||
isScrolling: false, | ||
handleClosingBottomSheet: null, | ||
|
@@ -89,16 +96,66 @@ class BottomSheet extends Component { | |
Dimensions.addEventListener( 'change', this.onDimensionsChange ); | ||
} | ||
|
||
keyboardWillShow( e ) { | ||
keyboardShow( e ) { | ||
if ( ! this.props.isVisible ) { | ||
return; | ||
} | ||
Comment on lines
+101
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided to prevent executing any logic related to keyboard show/hide events for non-visible bottom-sheets. Not sure if there would be a case that would require this but at least we should prevent triggering layout animations because they're global. |
||
|
||
const { height } = e.endCoordinates; | ||
this.keyboardHeight = height; | ||
this.performKeyboardLayoutAnimation( e ); | ||
this.onSetMaxHeight(); | ||
this.props.onKeyboardShow?.(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This callback and |
||
} | ||
|
||
this.setState( { keyboardHeight: height }, () => | ||
this.onSetMaxHeight() | ||
); | ||
keyboardHide( e ) { | ||
if ( ! this.props.isVisible ) { | ||
return; | ||
} | ||
|
||
this.keyboardHeight = 0; | ||
this.performKeyboardLayoutAnimation( e ); | ||
this.onSetMaxHeight(); | ||
this.props.onKeyboardHide?.(); | ||
} | ||
|
||
performKeyboardLayoutAnimation( event ) { | ||
const { duration, easing } = event; | ||
|
||
if ( duration && easing ) { | ||
const animationConfig = { | ||
// We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m | ||
duration: duration > 10 ? duration : 10, | ||
type: LayoutAnimation.Types[ easing ] || 'keyboard', | ||
}; | ||
const layoutAnimation = { | ||
duration: animationConfig.duration, | ||
update: animationConfig, | ||
create: { | ||
...animationConfig, | ||
property: LayoutAnimation.Properties.opacity, | ||
}, | ||
delete: { | ||
...animationConfig, | ||
property: LayoutAnimation.Properties.opacity, | ||
}, | ||
}; | ||
LayoutAnimation.configureNext( layoutAnimation ); | ||
this.lastLayoutAnimation = layoutAnimation; | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought the original note in your other branch that this code came form React Native's internal There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided to remove it because I wasn't really applying the same code but I agree that it would be helpful to keep the reference 👍 , I reverted the change in this commit. |
||
this.performRegularLayoutAnimation( { | ||
useLastLayoutAnimation: false, | ||
} ); | ||
} | ||
} | ||
|
||
keyboardDidHide() { | ||
this.setState( { keyboardHeight: 0 }, () => this.onSetMaxHeight() ); | ||
performRegularLayoutAnimation( { useLastLayoutAnimation } ) { | ||
const layoutAnimation = useLastLayoutAnimation | ||
? this.lastLayoutAnimation || DEFAULT_LAYOUT_ANIMATION | ||
: DEFAULT_LAYOUT_ANIMATION; | ||
|
||
LayoutAnimation.configureNext( layoutAnimation ); | ||
this.lastLayoutAnimation = layoutAnimation; | ||
} | ||
|
||
componentDidMount() { | ||
|
@@ -110,14 +167,14 @@ class BottomSheet extends Component { | |
); | ||
} | ||
|
||
this.keyboardWillShowListener = Keyboard.addListener( | ||
'keyboardWillShow', | ||
this.keyboardWillShow | ||
this.keyboardShowListener = Keyboard.addListener( | ||
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', | ||
this.keyboardShow | ||
); | ||
|
||
this.keyboardDidHideListener = Keyboard.addListener( | ||
'keyboardDidHide', | ||
this.keyboardDidHide | ||
this.keyboardHideListener = Keyboard.addListener( | ||
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', | ||
this.keyboardHide | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be worthwhile leaving a code comment inline stating this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea! I've added the comment in this commit. |
||
); | ||
|
||
this.safeAreaEventSubscription = SafeArea.addEventListener( | ||
|
@@ -128,8 +185,8 @@ class BottomSheet extends Component { | |
} | ||
|
||
componentWillUnmount() { | ||
this.keyboardWillShowListener.remove(); | ||
this.keyboardDidHideListener.remove(); | ||
this.keyboardShowListener.remove(); | ||
this.keyboardHideListener.remove(); | ||
if ( this.androidModalClosedSubscription ) { | ||
this.androidModalClosedSubscription.remove(); | ||
} | ||
|
@@ -163,16 +220,17 @@ class BottomSheet extends Component { | |
|
||
onSetMaxHeight() { | ||
const { height, width } = Dimensions.get( 'window' ); | ||
const { safeAreaBottomInset, keyboardHeight } = this.state; | ||
const { safeAreaBottomInset } = this.state; | ||
const statusBarHeight = | ||
Platform.OS === 'android' ? StatusBar.currentHeight : 0; | ||
|
||
// `maxHeight` when modal is opened along with a keyboard | ||
const maxHeightWithOpenKeyboard = | ||
0.95 * | ||
( Dimensions.get( 'window' ).height - | ||
keyboardHeight - | ||
statusBarHeight ); | ||
this.keyboardHeight - | ||
statusBarHeight - | ||
this.headerHeight ); | ||
|
||
// On horizontal mode `maxHeight` has to be set on 90% of width | ||
if ( width > height ) { | ||
|
@@ -195,6 +253,15 @@ class BottomSheet extends Component { | |
this.setState( { bounces: false } ); | ||
} | ||
|
||
onHeaderLayout( { nativeEvent } ) { | ||
const { height } = nativeEvent.layout; | ||
this.headerHeight = height; | ||
this.performRegularLayoutAnimation( { | ||
useLastLayoutAnimation: true, | ||
} ); | ||
this.onSetMaxHeight(); | ||
} | ||
|
||
isCloseToBottom( { layoutMeasurement, contentOffset, contentSize } ) { | ||
return ( | ||
layoutMeasurement.height + contentOffset.y >= | ||
|
@@ -293,6 +360,7 @@ class BottomSheet extends Component { | |
isVisible, | ||
leftButton, | ||
rightButton, | ||
header, | ||
hideHeader, | ||
style = {}, | ||
contentStyle = {}, | ||
|
@@ -375,16 +443,18 @@ class BottomSheet extends Component { | |
|
||
const getHeader = () => ( | ||
<> | ||
<View style={ styles.bottomSheetHeader }> | ||
<View style={ styles.flex }>{ leftButton }</View> | ||
<Text | ||
style={ bottomSheetHeaderTitleStyle } | ||
maxFontSizeMultiplier={ 3 } | ||
> | ||
{ title } | ||
</Text> | ||
<View style={ styles.flex }>{ rightButton }</View> | ||
</View> | ||
{ header || ( | ||
<View style={ styles.bottomSheetHeader }> | ||
<View style={ styles.flex }>{ leftButton }</View> | ||
<Text | ||
style={ bottomSheetHeaderTitleStyle } | ||
maxFontSizeMultiplier={ 3 } | ||
> | ||
{ title } | ||
</Text> | ||
<View style={ styles.flex }>{ rightButton }</View> | ||
</View> | ||
) } | ||
{ withHeaderSeparator && <View style={ styles.separator } /> } | ||
</> | ||
); | ||
|
@@ -434,10 +504,12 @@ class BottomSheet extends Component { | |
} } | ||
keyboardVerticalOffset={ -safeAreaBottomInset } | ||
> | ||
{ ! ( Platform.OS === 'android' && isFullScreen ) && ( | ||
<View style={ styles.dragIndicator } /> | ||
) } | ||
{ ! hideHeader && getHeader() } | ||
<View onLayout={ this.onHeaderLayout }> | ||
{ ! ( Platform.OS === 'android' && isFullScreen ) && ( | ||
<View style={ styles.dragIndicator } /> | ||
) } | ||
{ ! hideHeader && getHeader() } | ||
</View> | ||
<WrapperView | ||
{ ...( hasNavigation | ||
? { style: listProps.style } | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We no longer need to add the search form height here because this value is now included in the calculations of the
maxHeight
, which is part of the style propcontentContainerStyle
.