From cc6efdcfd49d26eedbbc265a45ca2b19225f5152 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 26 Mar 2021 11:57:09 +0100 Subject: [PATCH 01/11] Add header prop to BottomSheet --- .../src/mobile/bottom-sheet/index.native.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index 2f5aba85f9ab6..6ef186ec46dc6 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -293,6 +293,7 @@ class BottomSheet extends Component { isVisible, leftButton, rightButton, + header, hideHeader, style = {}, contentStyle = {}, @@ -375,16 +376,18 @@ class BottomSheet extends Component { const getHeader = () => ( <> - - { leftButton } - - { title } - - { rightButton } - + { header || ( + + { leftButton } + + { title } + + { rightButton } + + ) } { withHeaderSeparator && } ); From 17ae9a43a2c32c9f388ab63416163584e65b86e1 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 26 Mar 2021 12:00:20 +0100 Subject: [PATCH 02/11] Add header height to max height calculation --- .../src/mobile/bottom-sheet/index.native.js | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index 6ef186ec46dc6..9d6b248b790c9 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -58,6 +58,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 @@ -69,12 +70,14 @@ class BottomSheet extends Component { this.keyboardWillShow = this.keyboardWillShow.bind( this ); this.keyboardDidHide = this.keyboardDidHide.bind( this ); + this.headerHeight = 0; + this.keyboardHeight = 0; + this.state = { safeAreaBottomInset: 0, safeAreaTopInset: 0, bounces: false, maxHeight: 0, - keyboardHeight: 0, scrollEnabled: true, isScrolling: false, handleClosingBottomSheet: null, @@ -92,13 +95,13 @@ class BottomSheet extends Component { keyboardWillShow( e ) { const { height } = e.endCoordinates; - this.setState( { keyboardHeight: height }, () => - this.onSetMaxHeight() - ); + this.keyboardHeight = height; + this.onSetMaxHeight(); } keyboardDidHide() { - this.setState( { keyboardHeight: 0 }, () => this.onSetMaxHeight() ); + this.keyboardHeight = 0; + this.onSetMaxHeight(); } componentDidMount() { @@ -163,7 +166,7 @@ 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; @@ -171,8 +174,9 @@ class BottomSheet extends Component { 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 +199,12 @@ class BottomSheet extends Component { this.setState( { bounces: false } ); } + onHeaderLayout( { nativeEvent } ) { + const { height } = nativeEvent.layout; + this.headerHeight = height; + this.onSetMaxHeight(); + } + isCloseToBottom( { layoutMeasurement, contentOffset, contentSize } ) { return ( layoutMeasurement.height + contentOffset.y >= @@ -437,10 +447,12 @@ class BottomSheet extends Component { } } keyboardVerticalOffset={ -safeAreaBottomInset } > - { ! ( Platform.OS === 'android' && isFullScreen ) && ( - - ) } - { ! hideHeader && getHeader() } + + { ! ( Platform.OS === 'android' && isFullScreen ) && ( + + ) } + { ! hideHeader && getHeader() } + Date: Fri, 26 Mar 2021 12:04:57 +0100 Subject: [PATCH 03/11] Move inserter search form to bottom sheet header --- .../src/components/inserter/menu.native.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index 73bb0e074281b..f997692ee21ca 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -181,26 +181,26 @@ function InserterMenu( { { + setFilterValue( value ); + } } + value={ filterValue } + onLayout={ ( event ) => { + const { height } = event.nativeEvent.layout; + setSearchFormHeight( height ); + } } + /> + ) + } hasNavigation setMinHeightToMaxHeight={ showSearchForm } > { ( { listProps, safeAreaBottomInset } ) => ( - { showSearchForm && ( - { - setFilterValue( value ); - } } - value={ filterValue } - onLayout={ ( event ) => { - const { height } = event.nativeEvent.layout; - setSearchFormHeight( height ); - } } - /> - ) } - { From c93082aa92ffa0a1ee70e36f4815a09df73fa882 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 30 Apr 2021 14:02:43 +0200 Subject: [PATCH 04/11] Add layout animations to bottom sheet component --- .../src/mobile/bottom-sheet/index.native.js | 86 +++++++++++++++---- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index 9d6b248b790c9..bf6edc3d89823 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -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 ); @@ -67,11 +70,12 @@ 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, @@ -92,18 +96,59 @@ class BottomSheet extends Component { Dimensions.addEventListener( 'change', this.onDimensionsChange ); } - keyboardWillShow( e ) { - const { height } = e.endCoordinates; + keyboardShow( e ) { + if ( ! this.props.isVisible ) { + return; + } + const { height } = e.endCoordinates; this.keyboardHeight = height; + this.performKeyboardLayoutAnimation( e ); this.onSetMaxHeight(); } - keyboardDidHide() { + keyboardHide( e ) { + if ( ! this.props.isVisible ) { + return; + } + this.keyboardHeight = 0; + this.performKeyboardLayoutAnimation( e ); this.onSetMaxHeight(); } + // This layout animation is the same as the React Native's KeyboardAvoidingView component. + // Reference: https://github.com/facebook/react-native/blob/266b21baf35e052ff28120f79c06c4f6dddc51a9/Libraries/Components/Keyboard/KeyboardAvoidingView.js#L119-L128 + performKeyboardLayoutAnimation( event ) { + const { duration, easing } = event; + + if ( duration && easing ) { + const layoutAnimation = { + // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m + duration: duration > 10 ? duration : 10, + update: { + duration: duration > 10 ? duration : 10, + type: LayoutAnimation.Types[ easing ] || 'keyboard', + }, + }; + LayoutAnimation.configureNext( layoutAnimation ); + this.lastLayoutAnimation = layoutAnimation; + } else { + this.performRegularLayoutAnimation( { + useLastLayoutAnimation: false, + } ); + } + } + + performRegularLayoutAnimation( { useLastLayoutAnimation } ) { + const layoutAnimation = useLastLayoutAnimation + ? this.lastLayoutAnimation || DEFAULT_LAYOUT_ANIMATION + : DEFAULT_LAYOUT_ANIMATION; + + LayoutAnimation.configureNext( layoutAnimation ); + this.lastLayoutAnimation = layoutAnimation; + } + componentDidMount() { if ( Platform.OS === 'android' ) { this.androidModalClosedSubscription = subscribeAndroidModalClosed( @@ -113,14 +158,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 ); this.safeAreaEventSubscription = SafeArea.addEventListener( @@ -131,8 +176,8 @@ class BottomSheet extends Component { } componentWillUnmount() { - this.keyboardWillShowListener.remove(); - this.keyboardDidHideListener.remove(); + this.keyboardShowListener.remove(); + this.keyboardHideListener.remove(); if ( this.androidModalClosedSubscription ) { this.androidModalClosedSubscription.remove(); } @@ -202,6 +247,11 @@ class BottomSheet extends Component { onHeaderLayout( { nativeEvent } ) { const { height } = nativeEvent.layout; this.headerHeight = height; + if ( Platform.OS === 'ios' ) { + this.performRegularLayoutAnimation( { + useLastLayoutAnimation: true, + } ); + } this.onSetMaxHeight(); } From 2a70a386ebbd7246ee7aec50e9c6b3a348a41cf2 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 30 Apr 2021 15:43:53 +0200 Subject: [PATCH 05/11] Update keyboard layout animation --- .../src/mobile/bottom-sheet/index.native.js | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index bf6edc3d89823..a1fcce42e482f 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -117,18 +117,25 @@ class BottomSheet extends Component { this.onSetMaxHeight(); } - // This layout animation is the same as the React Native's KeyboardAvoidingView component. - // Reference: https://github.com/facebook/react-native/blob/266b21baf35e052ff28120f79c06c4f6dddc51a9/Libraries/Components/Keyboard/KeyboardAvoidingView.js#L119-L128 performKeyboardLayoutAnimation( event ) { const { duration, easing } = event; if ( duration && easing ) { - const layoutAnimation = { + const animationConfig = { // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m duration: duration > 10 ? duration : 10, - update: { - duration: duration > 10 ? duration : 10, - type: LayoutAnimation.Types[ easing ] || 'keyboard', + 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 ); @@ -247,11 +254,9 @@ class BottomSheet extends Component { onHeaderLayout( { nativeEvent } ) { const { height } = nativeEvent.layout; this.headerHeight = height; - if ( Platform.OS === 'ios' ) { - this.performRegularLayoutAnimation( { - useLastLayoutAnimation: true, - } ); - } + this.performRegularLayoutAnimation( { + useLastLayoutAnimation: true, + } ); this.onSetMaxHeight(); } From 736fab53db782f60d62f1aec3e04dbfbb56addde Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 30 Apr 2021 15:52:49 +0200 Subject: [PATCH 06/11] Add keyboard show/hide callbacks to bottom sheet --- packages/components/src/mobile/bottom-sheet/index.native.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index a1fcce42e482f..35ed8a074e94c 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -105,6 +105,7 @@ class BottomSheet extends Component { this.keyboardHeight = height; this.performKeyboardLayoutAnimation( e ); this.onSetMaxHeight(); + this.props.onKeyboardShow?.(); } keyboardHide( e ) { @@ -115,6 +116,7 @@ class BottomSheet extends Component { this.keyboardHeight = 0; this.performKeyboardLayoutAnimation( e ); this.onSetMaxHeight(); + this.props.onKeyboardHide?.(); } performKeyboardLayoutAnimation( event ) { From e35bff52f296d5156a8418bef57954f1f5cd219c Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Fri, 30 Apr 2021 16:24:01 +0200 Subject: [PATCH 07/11] Remove search form height This value that was being used for calculating the padding bottom of the search results view is no longer needed. --- .../block-editor/src/components/inserter/menu.native.js | 6 ------ .../src/components/inserter/search-form.native.js | 4 ++-- .../src/components/inserter/search-results.native.js | 4 +--- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index f997692ee21ca..a81cfbd184db8 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -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__ ); @@ -188,10 +187,6 @@ function InserterMenu( { setFilterValue( value ); } } value={ filterValue } - onLayout={ ( event ) => { - const { height } = event.nativeEvent.layout; - setSearchFormHeight( height ); - } } /> ) } @@ -210,7 +205,6 @@ function InserterMenu( { { ...{ listProps, safeAreaBottomInset, - searchFormHeight, } } /> diff --git a/packages/block-editor/src/components/inserter/search-form.native.js b/packages/block-editor/src/components/inserter/search-form.native.js index 8178aef4affc6..f23d6b95b9100 100644 --- a/packages/block-editor/src/components/inserter/search-form.native.js +++ b/packages/block-editor/src/components/inserter/search-form.native.js @@ -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(); @@ -43,7 +43,7 @@ function InserterSearchForm( { value, onChange, onLayout = () => {} } ) { ); return ( - + { isActive ? ( From c49b00716ff2eb62390b887cae3ada2d2528e3ac Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 4 May 2021 11:53:38 +0200 Subject: [PATCH 08/11] Prevent multiple layout animations on Android --- .../src/mobile/bottom-sheet/index.native.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index 35ed8a074e94c..b748570ca0a6a 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -76,6 +76,7 @@ class BottomSheet extends Component { this.headerHeight = 0; this.keyboardHeight = 0; this.lastLayoutAnimation = null; + this.lastLayoutAnimationFinished = false; this.state = { safeAreaBottomInset: 0, @@ -140,7 +141,10 @@ class BottomSheet extends Component { property: LayoutAnimation.Properties.opacity, }, }; - LayoutAnimation.configureNext( layoutAnimation ); + this.lastLayoutAnimationFinished = false; + LayoutAnimation.configureNext( layoutAnimation, () => { + this.lastLayoutAnimationFinished = true; + } ); this.lastLayoutAnimation = layoutAnimation; } else { this.performRegularLayoutAnimation( { @@ -150,11 +154,23 @@ class BottomSheet extends Component { } performRegularLayoutAnimation( { useLastLayoutAnimation } ) { + // On Android, we should prevent triggering multiple layout animations at the same time because it can produce visual glitches. + if ( + Platform.OS === 'android' && + this.lastLayoutAnimation && + ! this.lastLayoutAnimationFinished + ) { + return; + } + const layoutAnimation = useLastLayoutAnimation ? this.lastLayoutAnimation || DEFAULT_LAYOUT_ANIMATION : DEFAULT_LAYOUT_ANIMATION; - LayoutAnimation.configureNext( layoutAnimation ); + this.lastLayoutAnimationFinished = false; + LayoutAnimation.configureNext( layoutAnimation, () => { + this.lastLayoutAnimationFinished = true; + } ); this.lastLayoutAnimation = layoutAnimation; } From 7b865ade0da951d19a977c925a9655a112b0a6dd Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 4 May 2021 12:33:19 +0200 Subject: [PATCH 09/11] Revert removal of comment related to KeyboardAvoidingView --- packages/components/src/mobile/bottom-sheet/index.native.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index b748570ca0a6a..cc49e74044aef 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -124,6 +124,8 @@ class BottomSheet extends Component { const { duration, easing } = event; if ( duration && easing ) { + // This layout animation is the same as the React Native's KeyboardAvoidingView component. + // Reference: https://github.com/facebook/react-native/blob/266b21baf35e052ff28120f79c06c4f6dddc51a9/Libraries/Components/Keyboard/KeyboardAvoidingView.js#L119-L128 const animationConfig = { // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m duration: duration > 10 ? duration : 10, From 467e1b2926064ce452373fa510c8b778111cbb1a Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Tue, 4 May 2021 12:36:02 +0200 Subject: [PATCH 10/11] Add comment about "will" keyboard event availability --- packages/components/src/mobile/bottom-sheet/index.native.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index cc49e74044aef..40c64795bcb13 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -185,11 +185,12 @@ class BottomSheet extends Component { ); } + // 'Will' keyboard events are not available on Android. + // Reference: https://reactnative.dev/docs/0.61/keyboard#addlistener this.keyboardShowListener = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', this.keyboardShow ); - this.keyboardHideListener = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', this.keyboardHide From 9a89ce2f6545447a16942afe2ba4e8dff105b3f0 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 6 May 2021 10:20:21 +0200 Subject: [PATCH 11/11] Update CHANGELOG file --- packages/react-native-editor/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 5025c459837b6..6ff33e3fb685a 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [*] Bottom-sheet: Add custom header [#30291] ## 1.52.0