Skip to content
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

Merged
merged 12 commits into from
May 6, 2021
26 changes: 10 additions & 16 deletions packages/block-editor/src/components/inserter/menu.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ function InserterMenu( {
insertionIndex,
} ) {
const [ filterValue, setFilterValue ] = useState( '' );
const [ searchFormHeight, setSearchFormHeight ] = useState( 0 );
// eslint-disable-next-line no-undef
const [ showSearchForm, setShowSearchForm ] = useState( __DEV__ );

Expand Down Expand Up @@ -181,26 +180,22 @@ function InserterMenu( {
<BottomSheet
isVisible={ true }
onClose={ onClose }
hideHeader
header={
showSearchForm && (
<InserterSearchForm
onChange={ ( value ) => {
setFilterValue( value );
} }
value={ filterValue }
/>
)
}
hasNavigation
setMinHeightToMaxHeight={ showSearchForm }
>
<BottomSheetConsumer>
{ ( { listProps, safeAreaBottomInset } ) => (
<View>
{ showSearchForm && (
<InserterSearchForm
onChange={ ( value ) => {
setFilterValue( value );
} }
value={ filterValue }
onLayout={ ( event ) => {
const { height } = event.nativeEvent.layout;
setSearchFormHeight( height );
} }
/>
) }

<InserterSearchResults
items={ getItems() }
onSelect={ ( item ) => {
Expand All @@ -210,7 +205,6 @@ function InserterMenu( {
{ ...{
listProps,
safeAreaBottomInset,
searchFormHeight,
} }
/>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
*/
import styles from './style.scss';

function InserterSearchForm( { value, onChange, onLayout = () => {} } ) {
function InserterSearchForm( { value, onChange } ) {
const [ isActive, setIsActive ] = useState( false );

const inputRef = useRef();
Expand All @@ -43,7 +43,7 @@ function InserterSearchForm( { value, onChange, onLayout = () => {} } ) {
);

return (
<TouchableHighlight accessible={ false } onLayout={ onLayout }>
<TouchableHighlight accessible={ false }>
<View style={ searchFormStyle }>
{ isActive ? (
<ToolbarButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ function InserterSearchResults( {
onSelect,
listProps,
safeAreaBottomInset,
searchFormHeight = 0,
} ) {
const [ numberOfColumns, setNumberOfColumns ] = useState( MIN_COL_NUM );
const [ itemWidth, setItemWidth ] = useState();
Expand Down Expand Up @@ -104,8 +103,7 @@ function InserterSearchResults( {
...listProps.contentContainerStyle,
{
paddingBottom:
( safeAreaBottomInset ||
styles.list.paddingBottom ) + searchFormHeight,
safeAreaBottomInset || styles.list.paddingBottom,
Comment on lines -107 to +106
Copy link
Contributor Author

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 prop contentContainerStyle.

},
] }
/>
Expand Down
150 changes: 111 additions & 39 deletions packages/components/src/mobile/bottom-sheet/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 );
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?.();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This callback and onKeyboardHide will allow us to sync potential changes in the layout with the layout animations triggered from performKeyboardLayoutAnimation.

}

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 {
Copy link
Member

Choose a reason for hiding this comment

The 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 KeyboardAvoidingView was helpful, personally. It may be worth adding the comment that is the origin of this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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() {
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will keyboard events are not available on Android (documentation reference).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worthwhile leaving a code comment inline stating this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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(
Expand All @@ -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();
}
Expand Down Expand Up @@ -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 ) {
Expand All @@ -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 >=
Expand Down Expand Up @@ -293,6 +360,7 @@ class BottomSheet extends Component {
isVisible,
leftButton,
rightButton,
header,
hideHeader,
style = {},
contentStyle = {},
Expand Down Expand Up @@ -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 } /> }
</>
);
Expand Down Expand Up @@ -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 }
Expand Down