From 1ab42859a75e7b433bd81dfc5eb87afdaf0e7dc9 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 6 Mar 2023 16:06:38 +0100 Subject: [PATCH 01/48] Remove react-native-keyboard-aware-scroll-view dependency --- package-lock.json | 9 --------- .../ios/GutenbergDemo.xcodeproj/project.pbxproj | 4 ---- packages/react-native-editor/ios/Podfile.lock | 6 ------ packages/react-native-editor/package.json | 1 - 4 files changed, 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f314106697431..de8dd2469b4829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18403,7 +18403,6 @@ "react-native-get-random-values": "1.4.0", "react-native-hr": "https://raw.githubusercontent.com/wordpress-mobile/react-native-hr/1.1.3-wp-1/react-native-hr-1.1.3.tgz", "react-native-hsv-color-picker": "https://raw.githubusercontent.com/wordpress-mobile/react-native-hsv-color-picker/v1.0.1-wp-3/react-native-hsv-color-picker-1.0.1-wp-3.tgz", - "react-native-keyboard-aware-scroll-view": "https://raw.githubusercontent.com/wordpress-mobile/react-native-keyboard-aware-scroll-view/v0.8.8-wp-1/react-native-keyboard-aware-scroll-view-0.8.8-wp-1.tgz", "react-native-linear-gradient": "https://raw.githubusercontent.com/wordpress-mobile/react-native-linear-gradient/v2.5.6-wp-3/react-native-linear-gradient-2.5.6-wp-3.tgz", "react-native-modal": "^11.10.0", "react-native-prompt-android": "https://raw.githubusercontent.com/wordpress-mobile/react-native-prompt-android/v1.0.0-wp-3/react-native-prompt-android-1.0.0-wp-3.tgz", @@ -51790,14 +51789,6 @@ "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz", "integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==" }, - "react-native-keyboard-aware-scroll-view": { - "version": "https://raw.githubusercontent.com/wordpress-mobile/react-native-keyboard-aware-scroll-view/v0.8.8-wp-1/react-native-keyboard-aware-scroll-view-0.8.8-wp-1.tgz", - "integrity": "sha512-gp0xGZvr4TKFW5K5HM2uPsSsbSdONOlajEZohWVxLJJyVSToyaptt/amJrvkpWFqlqtcE9k22iEO/vzpnAgNRw==", - "requires": { - "prop-types": "^15.6.2", - "react-native-iphone-x-helper": "^1.0.3" - } - }, "react-native-linear-gradient": { "version": "https://raw.githubusercontent.com/wordpress-mobile/react-native-linear-gradient/v2.5.6-wp-3/react-native-linear-gradient-2.5.6-wp-3.tgz", "integrity": "sha512-Xq/ABki6/zz6ejut2wPWrh2ZV9Cw5NhHsFcB1adhY/Z2YIVyAVnpApwhMWVV6BxbtKcl17eMPR6vpOI5Q76BjA==" diff --git a/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj b/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj index 010f3add38639f..79a27e1e618468 100644 --- a/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj +++ b/packages/react-native-editor/ios/GutenbergDemo.xcodeproj/project.pbxproj @@ -370,7 +370,6 @@ "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/react-native-blur/react_native_blur.framework", "${BUILT_PRODUCTS_DIR}/react-native-get-random-values/react_native_get_random_values.framework", - "${BUILT_PRODUCTS_DIR}/react-native-keyboard-aware-scroll-view/react_native_keyboard_aware_scroll_view.framework", "${BUILT_PRODUCTS_DIR}/react-native-safe-area/react_native_safe_area.framework", "${BUILT_PRODUCTS_DIR}/react-native-safe-area-context/react_native_safe_area_context.framework", "${BUILT_PRODUCTS_DIR}/react-native-slider/react_native_slider.framework", @@ -419,7 +418,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_blur.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_get_random_values.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_keyboard_aware_scroll_view.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area_context.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_slider.framework", @@ -500,7 +498,6 @@ "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/react-native-blur/react_native_blur.framework", "${BUILT_PRODUCTS_DIR}/react-native-get-random-values/react_native_get_random_values.framework", - "${BUILT_PRODUCTS_DIR}/react-native-keyboard-aware-scroll-view/react_native_keyboard_aware_scroll_view.framework", "${BUILT_PRODUCTS_DIR}/react-native-safe-area/react_native_safe_area.framework", "${BUILT_PRODUCTS_DIR}/react-native-safe-area-context/react_native_safe_area_context.framework", "${BUILT_PRODUCTS_DIR}/react-native-slider/react_native_slider.framework", @@ -549,7 +546,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_blur.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_get_random_values.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_keyboard_aware_scroll_view.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_safe_area_context.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_slider.framework", diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 403f06f95d2cb0..7d69dac96ee914 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -240,8 +240,6 @@ PODS: - React-Core - react-native-get-random-values (1.4.0): - React-Core - - react-native-keyboard-aware-scroll-view (0.8.8-wp-1): - - React-Core - react-native-safe-area (0.5.1): - React-Core - react-native-safe-area-context (3.2.0): @@ -399,7 +397,6 @@ DEPENDENCIES: - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) - "react-native-blur (from `../../../node_modules/@react-native-community/blur`)" - react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`) - - react-native-keyboard-aware-scroll-view (from `../../../node_modules/react-native-keyboard-aware-scroll-view`) - react-native-safe-area (from `../../../node_modules/react-native-safe-area`) - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) - "react-native-slider (from `../../../node_modules/@react-native-community/slider`)" @@ -482,8 +479,6 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@react-native-community/blur" react-native-get-random-values: :path: "../../../node_modules/react-native-get-random-values" - react-native-keyboard-aware-scroll-view: - :path: "../../../node_modules/react-native-keyboard-aware-scroll-view" react-native-safe-area: :path: "../../../node_modules/react-native-safe-area" react-native-safe-area-context: @@ -563,7 +558,6 @@ SPEC CHECKSUMS: React-logger: 1088859f145b8f6dd0d3ed051a647ef0e3e80fad react-native-blur: 3e9c8e8e9f7d17fa1b94e1a0ae9fd816675f5382 react-native-get-random-values: b6fb85e7169b9822976793e467458c151c3e8b69 - react-native-keyboard-aware-scroll-view: 0bc6c2dfe9056935a40dc1a70e764b7a1bbf6568 react-native-safe-area: c9cf765aa2dd96159476a99633e7d462ce5bb94f react-native-safe-area-context: f0906bf8bc9835ac9a9d3f97e8bde2a997d8da79 react-native-slider: a433f1c13c5da3c17a587351bff7371f65cc9a07 diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 8d83f7ada8aebd..9185bfb0b6a7f1 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -62,7 +62,6 @@ "react-native-get-random-values": "1.4.0", "react-native-hr": "https://raw.githubusercontent.com/wordpress-mobile/react-native-hr/1.1.3-wp-1/react-native-hr-1.1.3.tgz", "react-native-hsv-color-picker": "https://raw.githubusercontent.com/wordpress-mobile/react-native-hsv-color-picker/v1.0.1-wp-3/react-native-hsv-color-picker-1.0.1-wp-3.tgz", - "react-native-keyboard-aware-scroll-view": "https://raw.githubusercontent.com/wordpress-mobile/react-native-keyboard-aware-scroll-view/v0.8.8-wp-1/react-native-keyboard-aware-scroll-view-0.8.8-wp-1.tgz", "react-native-linear-gradient": "https://raw.githubusercontent.com/wordpress-mobile/react-native-linear-gradient/v2.5.6-wp-3/react-native-linear-gradient-2.5.6-wp-3.tgz", "react-native-modal": "^11.10.0", "react-native-prompt-android": "https://raw.githubusercontent.com/wordpress-mobile/react-native-prompt-android/v1.0.0-wp-3/react-native-prompt-android-1.0.0-wp-3.tgz", From 5af792e52a4b99e404e192536672ce0a57fe91a4 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 6 Mar 2023 16:13:59 +0100 Subject: [PATCH 02/48] Mobile - AztecView/AztecInputState: - Remove usage of onCaretVerticalPositionChange - Add support for adding a listener when the caret Y coordinate position changes --- .../react-native-aztec/src/AztecInputState.js | 55 +++++++++++++++++++ packages/react-native-aztec/src/AztecView.js | 7 +-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 973b8179ea5ec9..8ffe376e0e641c 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -6,8 +6,10 @@ import TextInputState from 'react-native/Libraries/Components/TextInput/TextInpu /** @typedef {import('@wordpress/element').RefObject} RefObject */ const focusChangeListeners = []; +const caretChangeListeners = []; let currentFocusedElement = null; +let currentCaretYCoordinate = null; /** * Adds a listener that will be called in the following cases: @@ -47,6 +49,37 @@ const notifyListeners = ( { isFocused } ) => { } ); }; +/** + * Adds a listener that will be called when the caret's Y position + * changes for the focused Aztec view. + * + * @param {Function} listener + */ +export const addCaretChangeListener = ( listener ) => { + caretChangeListeners.push( listener ); +}; + +/** + * Removes a listener from the caret change listeners list. + * + * @param {Function} listener + */ +export const removeCaretChangeListener = ( listener ) => { + const itemIndex = caretChangeListeners.indexOf( listener ); + if ( itemIndex !== -1 ) { + caretChangeListeners.splice( itemIndex, 1 ); + } +}; + +/** + * Notifies listeners about caret changes in focused Aztec view. + */ +const notifyCaretChangeListeners = () => { + caretChangeListeners.forEach( ( listener ) => { + listener( { caretY: getCurrentCaretYCoordinate() } ); + } ); +}; + /** * Determines if any Aztec view is focused. * @@ -100,6 +133,7 @@ export const focus = ( element ) => { */ export const blur = ( element ) => { TextInputState.blurTextInput( element ); + setCurrentCaretYCoordinate( null ); notifyInputChange(); }; @@ -111,3 +145,24 @@ export const blurCurrentFocusedElement = () => { blur( getCurrentFocusedElement() ); } }; + +/** + * Sets the current focused element caret's Y coordinate. + * + * @param {?number} yCoordinate Caret's Y coordinate. + */ +export const setCurrentCaretYCoordinate = ( yCoordinate ) => { + if ( isFocused() ) { + currentCaretYCoordinate = yCoordinate; + notifyCaretChangeListeners(); + } +}; + +/** + * Get the current focused element caret's Y coordinate. + * + * @return {?number} Current caret's Y Coordinate. + */ +export const getCurrentCaretYCoordinate = () => { + return currentCaretYCoordinate; +}; diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index 8ac7884dd89c63..a81739f34eca17 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -198,15 +198,10 @@ class AztecView extends Component { } if ( - this.props.onCaretVerticalPositionChange && this.selectionEndCaretY !== event.nativeEvent.selectionEndCaretY ) { const caretY = event.nativeEvent.selectionEndCaretY; - this.props.onCaretVerticalPositionChange( - event.nativeEvent.target, - caretY, - this.selectionEndCaretY - ); + AztecInputState.setCurrentCaretYCoordinate( caretY ); this.selectionEndCaretY = caretY; } } From 560c62de84c8ba8ce411e9c9945baafc85fd0faf Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 6 Mar 2023 16:14:55 +0100 Subject: [PATCH 03/48] Mobile - VisualEditor - Remove keyboard listeners and the usage of isAutoScrollEnabled in the state of the component --- .../components/visual-editor/index.native.js | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/packages/edit-post/src/components/visual-editor/index.native.js b/packages/edit-post/src/components/visual-editor/index.native.js index 886e56e493938b..b23b73c334e75a 100644 --- a/packages/edit-post/src/components/visual-editor/index.native.js +++ b/packages/edit-post/src/components/visual-editor/index.native.js @@ -3,10 +3,7 @@ */ import { Component } from '@wordpress/element'; import { BlockList } from '@wordpress/block-editor'; -/** - * External dependencies - */ -import { Keyboard } from 'react-native'; + /** * Internal dependencies */ @@ -16,36 +13,6 @@ export default class VisualEditor extends Component { constructor( props ) { super( props ); this.renderHeader = this.renderHeader.bind( this ); - this.keyboardDidShow = this.keyboardDidShow.bind( this ); - this.keyboardDidHide = this.keyboardDidHide.bind( this ); - - this.state = { - isAutoScrollEnabled: true, - }; - } - - componentDidMount() { - this.keyboardDidShow = Keyboard.addListener( - 'keyboardDidShow', - this.keyboardDidShow - ); - this.keyboardDidHideListener = Keyboard.addListener( - 'keyboardDidHide', - this.keyboardDidHide - ); - } - - componentWillUnmount() { - this.keyboardDidShow.remove(); - this.keyboardDidHideListener.remove(); - } - - keyboardDidShow() { - this.setState( { isAutoScrollEnabled: false } ); - } - - keyboardDidHide() { - this.setState( { isAutoScrollEnabled: true } ); } renderHeader() { @@ -55,13 +22,11 @@ export default class VisualEditor extends Component { render() { const { safeAreaBottomInset } = this.props; - const { isAutoScrollEnabled } = this.state; return ( ); } From 281acd3fe04cd4cf5190f49e8f06e86c88867c0d Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 6 Mar 2023 16:16:11 +0100 Subject: [PATCH 04/48] Mobile - RichText - Remove usage useNativeProps which used to handle the caret position's changes for iOS --- .../src/components/rich-text/index.native.js | 5 ++--- .../components/rich-text/use-native-props.js | 3 --- .../rich-text/use-native-props.native.js | 17 ----------------- .../rich-text/src/component/index.native.js | 3 --- 4 files changed, 2 insertions(+), 26 deletions(-) delete mode 100644 packages/block-editor/src/components/rich-text/use-native-props.js delete mode 100644 packages/block-editor/src/components/rich-text/use-native-props.native.js diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 1ea26ddcf77750..b49d1ed793649a 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -46,7 +46,6 @@ import { useBlockEditContext } from '../block-edit'; import { RemoveBrowserShortcuts } from './remove-browser-shortcuts'; import { filePasteHandler } from './file-paste-handler'; import FormatToolbarContainer from './format-toolbar-container'; -import { useNativeProps } from './use-native-props'; import { store as blockEditorStore } from '../../store'; import { addActiveFormats, @@ -121,7 +120,6 @@ function RichTextWrapper( const fallbackRef = useRef(); const { clientId, isSelected: blockIsSelected } = useBlockEditContext(); - const nativeProps = useNativeProps(); const embedHandlerPickerRef = useRef(); const selector = ( select ) => { const { @@ -220,6 +218,7 @@ function RichTextWrapper( selectionChangeEnd ); }, + // eslint-disable-next-line react-hooks/exhaustive-deps [ clientId, identifier ] ); @@ -373,6 +372,7 @@ function RichTextWrapper( } } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [ onReplace, onSplit, @@ -615,7 +615,6 @@ function RichTextWrapper( } __unstableMultilineRootTag={ __unstableMultilineRootTag } // Native props. - { ...nativeProps } blockIsSelected={ originalIsSelected !== undefined ? originalIsSelected diff --git a/packages/block-editor/src/components/rich-text/use-native-props.js b/packages/block-editor/src/components/rich-text/use-native-props.js deleted file mode 100644 index 04343773a04c60..00000000000000 --- a/packages/block-editor/src/components/rich-text/use-native-props.js +++ /dev/null @@ -1,3 +0,0 @@ -export function useNativeProps() { - return {}; -} diff --git a/packages/block-editor/src/components/rich-text/use-native-props.native.js b/packages/block-editor/src/components/rich-text/use-native-props.native.js deleted file mode 100644 index 41f4e2ea9ac2cb..00000000000000 --- a/packages/block-editor/src/components/rich-text/use-native-props.native.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * WordPress dependencies - */ -import { useContext } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { OnCaretVerticalPositionChange } from '../block-list'; - -export function useNativeProps() { - return { - onCaretVerticalPositionChange: useContext( - OnCaretVerticalPositionChange - ), - }; -} diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index 9f64f87b414923..314131bc3d91e9 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -1221,9 +1221,6 @@ export class RichText extends Component { onPaste={ this.onPaste } activeFormats={ this.getActiveFormatNames( record ) } onContentSizeChange={ this.onContentSizeChange } - onCaretVerticalPositionChange={ - this.props.onCaretVerticalPositionChange - } onSelectionChange={ this.onSelectionChangeFromAztec } blockType={ { tag: tagName } } color={ From e6f15cde122d0791024f86c097fe19f9a1d8eed2 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 6 Mar 2023 16:17:59 +0100 Subject: [PATCH 05/48] Mobile - BlockList and Keyboard Aware FlatList for iOS: - Removes usage of the react-native-keyboard-aware-scroll-view library - Adds custom implementation to scroll to the focused TextInput element --- .../src/components/block-list/index.native.js | 51 +---- .../keyboard-aware-flat-list/index.android.js | 4 - .../keyboard-aware-flat-list/index.ios.js | 198 ++++++++++++------ 3 files changed, 138 insertions(+), 115 deletions(-) diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index b562daedcf30cd..ba0b19c1fc3b9a 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -6,7 +6,7 @@ import { View, Platform, TouchableWithoutFeedback } from 'react-native'; /** * WordPress dependencies */ -import { Component, createContext } from '@wordpress/element'; +import { Component } from '@wordpress/element'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { createBlock } from '@wordpress/blocks'; @@ -33,7 +33,6 @@ import { import { BlockDraggableWrapper } from '../block-draggable'; import { store as blockEditorStore } from '../../store'; -export const OnCaretVerticalPositionChange = createContext(); const identity = ( x ) => x; const stylesMemo = {}; @@ -70,12 +69,8 @@ export class BlockList extends Component { }; this.renderItem = this.renderItem.bind( this ); this.renderBlockListFooter = this.renderBlockListFooter.bind( this ); - this.onCaretVerticalPositionChange = - this.onCaretVerticalPositionChange.bind( this ); this.scrollViewInnerRef = this.scrollViewInnerRef.bind( this ); this.addBlockToEndOfPost = this.addBlockToEndOfPost.bind( this ); - this.shouldFlatListPreventAutomaticScroll = - this.shouldFlatListPreventAutomaticScroll.bind( this ); this.shouldShowInnerBlockAppender = this.shouldShowInnerBlockAppender.bind( this ); this.renderEmptyList = this.renderEmptyList.bind( this ); @@ -94,23 +89,10 @@ export class BlockList extends Component { this.props.insertBlock( newBlock, this.props.blockCount ); } - onCaretVerticalPositionChange( targetId, caretY, previousCaretY ) { - KeyboardAwareFlatList.handleCaretVerticalPositionChange( - this.scrollViewRef, - targetId, - caretY, - previousCaretY - ); - } - scrollViewInnerRef( ref ) { this.scrollViewRef = ref; } - shouldFlatListPreventAutomaticScroll() { - return this.props.isBlockInsertionPointVisible; - } - shouldShowInnerBlockAppender() { const { blockClientIds, renderAppender } = this.props; return renderAppender && blockClientIds.length > 0; @@ -209,13 +191,7 @@ export class BlockList extends Component { ); - return ( - - { blockList } - - ); + return blockList; } renderList( extraProps = {} ) { @@ -250,6 +226,10 @@ export class BlockList extends Component { const isContentStretch = contentResizeMode === 'stretch'; const isMultiBlocks = blockClientIds.length > 1; const { isWider } = alignmentHelpers; + const extraScrollHeight = blockToolbar.height + blockBorder.width; + const inputAccessoryViewHeight = + headerToolbar.height + + ( isFloatingToolbarVisible ? floatingToolbar.height : 0 ); return ( { this.scrollViewInnerRef( parentScrollRef || ref ); } } - extraScrollHeight={ - blockToolbar.height + blockBorder.width - } - inputAccessoryViewHeight={ - headerToolbar.height + - ( isFloatingToolbarVisible - ? floatingToolbar.height - : 0 ) - } + extraScrollHeight={ extraScrollHeight } + inputAccessoryViewHeight={ inputAccessoryViewHeight } keyboardShouldPersistTaps="always" - scrollViewStyle={ [ - { flex: isRootList ? 1 : 0 }, - ! isRootList && styles.overflowVisible, - ] } extraData={ this.getExtraData() } scrollEnabled={ isRootList } contentContainerStyle={ [ @@ -299,9 +267,6 @@ export class BlockList extends Component { keyExtractor={ identity } renderItem={ this.renderItem } CellRendererComponent={ this.getCellRendererComponent } - shouldPreventAutomaticScroll={ - this.shouldFlatListPreventAutomaticScroll - } title={ title } ListHeaderComponent={ header } ListEmptyComponent={ ! isReadOnly && this.renderEmptyList } diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js index ffdd97dd5acbb7..eccb80f3903e5d 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js @@ -24,8 +24,4 @@ export const KeyboardAwareFlatList = ( { innerRef, onScroll, ...props } ) => { ); }; -KeyboardAwareFlatList.handleCaretVerticalPositionChange = () => { - // no need to handle on Android, it is system managed -}; - export default KeyboardAwareFlatList; diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 05ff4c8bb65191..f9cea7a761eb92 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -1,9 +1,8 @@ /** * External dependencies */ -import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; -import { FlatList } from 'react-native'; -import fastDeepEqual from 'fast-deep-equal/es6'; + +import { FlatList, Keyboard, useWindowDimensions } from 'react-native'; import Animated, { useAnimatedScrollHandler, useSharedValue, @@ -12,28 +11,33 @@ import Animated, { /** * WordPress dependencies */ -import { memo, useCallback, useRef } from '@wordpress/element'; +import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; +import RCTAztecView from '@wordpress/react-native-aztec'; -const List = memo( FlatList, fastDeepEqual ); -const AnimatedKeyboardAwareScrollView = Animated.createAnimatedComponent( - KeyboardAwareScrollView -); +const AnimatedFlatList = Animated.createAnimatedComponent( FlatList ); export const KeyboardAwareFlatList = ( { extraScrollHeight, - shouldPreventAutomaticScroll, innerRef, - autoScroll, - scrollViewStyle, inputAccessoryViewHeight, onScroll, - ...listProps + scrollEnabled, + shouldPreventAutomaticScroll, + ...props } ) => { - const scrollViewRef = useRef(); - const keyboardWillShowIndicator = useRef(); + const [ keyboardSpace, setKeyboardSpace ] = useState( 0 ); + const { height: windowHeight } = useWindowDimensions(); + + const listRef = useRef(); + const isEditingText = useRef( RCTAztecView.InputState.isFocused() ); + const isKeyboardVisible = useRef( false ); + const currentCaretYCoordinate = useRef( null ); const latestContentOffsetY = useSharedValue( -1 ); + const offsetExtraSpace = extraScrollHeight + inputAccessoryViewHeight; + const screenOffset = windowHeight - ( keyboardSpace + offsetExtraSpace ); + const scrollHandler = useAnimatedScrollHandler( { onScroll: ( event ) => { const { contentOffset } = event; @@ -42,70 +46,128 @@ export const KeyboardAwareFlatList = ( { }, } ); + useEffect( () => { + let willShowSubscription; + let showSubscription; + let hideSubscription; + + if ( scrollEnabled ) { + willShowSubscription = Keyboard.addListener( + 'keyboardWillShow', + () => { + isKeyboardVisible.current = true; + } + ); + showSubscription = Keyboard.addListener( + 'keyboardDidShow', + ( { endCoordinates } ) => { + if ( keyboardSpace === 0 && endCoordinates.height !== 0 ) { + setKeyboardSpace( endCoordinates.height ); + } + } + ); + hideSubscription = Keyboard.addListener( 'keyboardWillHide', () => { + if ( ! RCTAztecView.InputState.isFocused() ) { + setKeyboardSpace( 0 ); + } + isKeyboardVisible.current = false; + } ); + } + return () => { + willShowSubscription?.remove(); + showSubscription?.remove(); + hideSubscription?.remove(); + }; + }, [ scrollEnabled, keyboardSpace, isKeyboardVisible ] ); + + const onScrollToInput = useCallback( () => { + if ( + ! isEditingText.current || + ! scrollEnabled || + ! listRef.current || + ( isKeyboardVisible.current && keyboardSpace === 0 ) + ) { + return; + } + + const textInput = RCTAztecView.InputState.getCurrentFocusedElement(); + if ( textInput ) { + textInput.measureInWindow( ( _x, y, _width, height ) => { + const caretYPosition = + currentCaretYCoordinate.current || height; + const textInputOffset = y + caretYPosition; + + const offset = + latestContentOffsetY.value + + ( textInputOffset - screenOffset ); + + if ( offset > 0 && textInputOffset > screenOffset ) { + listRef.current.scrollToOffset( { + x: 0, + offset, + animated: true, + } ); + } + } ); + } + }, [ scrollEnabled, screenOffset, keyboardSpace, latestContentOffsetY ] ); + + useEffect( () => { + if ( keyboardSpace !== 0 ) { + onScrollToInput(); + } + }, [ keyboardSpace, onScrollToInput ] ); + + const onCaretChange = useCallback( + ( { caretY } ) => { + const isFocused = + !! RCTAztecView.InputState.getCurrentFocusedElement(); + isEditingText.current = isFocused; + + if ( ! isFocused ) { + return; + } + + currentCaretYCoordinate.current = caretY; + onScrollToInput(); + }, + [ onScrollToInput ] + ); + + useEffect( () => { + if ( scrollEnabled ) { + RCTAztecView.InputState.addCaretChangeListener( onCaretChange ); + } + + return () => { + if ( scrollEnabled ) { + RCTAztecView.InputState.removeCaretChangeListener( + onCaretChange + ); + } + }; + }, [ scrollEnabled, onCaretChange ] ); + const getRef = useCallback( ( ref ) => { - scrollViewRef.current = ref; + listRef.current = ref; innerRef( ref ); }, [ innerRef ] ); - const onKeyboardWillHide = useCallback( () => { - keyboardWillShowIndicator.current = false; - }, [] ); - const onKeyboardDidHide = useCallback( () => { - setTimeout( () => { - if ( - ! keyboardWillShowIndicator.current && - latestContentOffsetY.value !== -1 && - ! shouldPreventAutomaticScroll() - ) { - // Reset the content position if keyboard is still closed. - scrollViewRef.current?.scrollToPosition( - 0, - latestContentOffsetY.value, - true - ); - } - }, 50 ); - }, [ latestContentOffsetY, shouldPreventAutomaticScroll ] ); - const onKeyboardWillShow = useCallback( () => { - keyboardWillShowIndicator.current = true; - }, [] ); + + const contentInset = { bottom: keyboardSpace }; return ( - - - + ref={ getRef } + scrollEnabled={ scrollEnabled } + /> ); }; -KeyboardAwareFlatList.handleCaretVerticalPositionChange = ( - scrollView, - targetId, - caretY, - previousCaretY -) => { - if ( previousCaretY ) { - // If this is not the first tap. - scrollView.refreshScrollForField( targetId ); - } -}; - export default KeyboardAwareFlatList; From 1f452fdb2c15f07b3e7e52db48e5fb35751ca087 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 6 Mar 2023 16:25:11 +0100 Subject: [PATCH 06/48] Mobile - Remove patch for react-native-keyboard-aware-scroll-view --- ...eyboard-aware-scroll-view+0.8.8-wp-1.patch | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100644 patches/react-native-keyboard-aware-scroll-view+0.8.8-wp-1.patch diff --git a/patches/react-native-keyboard-aware-scroll-view+0.8.8-wp-1.patch b/patches/react-native-keyboard-aware-scroll-view+0.8.8-wp-1.patch deleted file mode 100644 index fab58b9d3157d9..00000000000000 --- a/patches/react-native-keyboard-aware-scroll-view+0.8.8-wp-1.patch +++ /dev/null @@ -1,76 +0,0 @@ -diff --git a/node_modules/react-native-keyboard-aware-scroll-view/lib/KeyboardAwareHOC.js b/node_modules/react-native-keyboard-aware-scroll-view/lib/KeyboardAwareHOC.js -index 30f62c9..83a6920 100644 ---- a/node_modules/react-native-keyboard-aware-scroll-view/lib/KeyboardAwareHOC.js -+++ b/node_modules/react-native-keyboard-aware-scroll-view/lib/KeyboardAwareHOC.js -@@ -264,9 +264,13 @@ function KeyboardAwareHOC( - }) - } - -- componentWillReceiveProps(nextProps: KeyboardAwareHOCProps) { -- if (nextProps.viewIsInsideTabBar !== this.props.viewIsInsideTabBar) { -- const keyboardSpace: number = nextProps.viewIsInsideTabBar -+ // This patch changed from the deprecated `componentWillReceiveProps` to -+ // `componentDidUpdate`. We can remove this patch when we upgrade to -+ // `react-native-keyboard-aware-scroll-view@^0.9.2` -+ // https://git.io/JPbOK -+ componentDidUpdate(prevProps: KeyboardAwareHOCProps) { -+ if (this.props.viewIsInsideTabBar !== prevProps.viewIsInsideTabBar) { -+ const keyboardSpace: number = this.props.viewIsInsideTabBar - ? _KAM_DEFAULT_TAB_BAR_HEIGHT - : 0 - if (this.state.keyboardSpace !== keyboardSpace) { -@@ -293,12 +297,33 @@ function KeyboardAwareHOC( - - scrollToPosition = (x: number, y: number, animated: boolean = true) => { - const responder = this.getScrollResponder() -- responder && responder.scrollResponderScrollTo({ x, y, animated }) -+ // Patch applied to avoid invoking the removed `scrollResponderScrollTo` -+ // method. This patch could be removed if we upgrade to -+ // `react-native-keyboard-aware-view@^0.9.5` https://git.io/JPb6a -+ if (!responder) return; -+ if (responder.scrollResponderScrollTo) { -+ // React Native < 0.65 -+ responder.scrollResponderScrollTo({ x, y, animated }) -+ } else if (responder.scrollTo) { -+ // React Native >= 0.65 -+ responder.scrollTo({ x, y, animated }) -+ } - } - - scrollToEnd = (animated?: boolean = true) => { - const responder = this.getScrollResponder() -- responder && responder.scrollResponderScrollToEnd({ animated }) -+ // Patch applied to avoid invoking the removed -+ // `scrollResponderScrollToEnd` method. This patch could be removed if we -+ // upgrade to `react-native-keyboard-aware-view@^0.9.5` -+ // https://git.io/JPb6a -+ if (!responder) return; -+ if (responder.scrollResponderScrollToEnd) { -+ // React Native < 0.65 -+ responder.scrollResponderScrollToEnd({ animated }) -+ } else if (responder.scrollToEnd) { -+ // React Native >= 0.65 -+ responder.scrollToEnd({ animated }) -+ } - } - - scrollForExtraHeightOnAndroid = (extraHeight: number) => { -@@ -553,7 +578,17 @@ function KeyboardAwareHOC( - - scrollOffsetY = Math.max(0, scrollOffsetY); //prevent negative scroll offset - const responder = this.getScrollResponder(); -- responder && responder.scrollResponderScrollTo( { x: 0, y: scrollOffsetY, animated: true } ); -+ // Patch applied to avoid invoking the removed `scrollResponderScrollTo` -+ // method. This patch could be removed if we upgrade to -+ // `react-native-keyboard-aware-view@^0.9.5` https://git.io/JPb6a -+ if (!responder) return; -+ if (responder.scrollResponderScrollTo) { -+ // React Native < 0.65 -+ responder.scrollResponderScrollTo( { x: 0, y: scrollOffsetY, animated: true } ) -+ } else if (responder.scrollTo) { -+ // React Native >= 0.65 -+ responder.scrollTo( { x: 0, y: scrollOffsetY, animated: true } ) -+ } - } - - const measureLayoutErrorHandler = ( e: Object ) => { From 9c685aeab11f7552830066c6accd209e68577de7 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 7 Mar 2023 17:33:12 +0100 Subject: [PATCH 07/48] Mobile - Update Block insertion E2E test --- ...utenberg-editor-block-insertion-@canary.test.js | 14 +++++++++----- .../__device-tests__/pages/editor-page.js | 13 +++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js index e5b34078a0e453..311196345f6b13 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js @@ -43,7 +43,9 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - await paragraphBlockElement.click(); + if ( isAndroid() ) { + await paragraphBlockElement.click(); + } await editorPage.removeBlock(); } } ); @@ -65,14 +67,14 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { } ); await titleElement.click(); + // Wait for editor to finish scrolling to the title + await editorPage.driver.sleep( 2000 ); + await editorPage.addNewBlock( blockNames.paragraph ); const emptyParagraphBlock = await editorPage.getBlockAtPosition( blockNames.paragraph ); expect( emptyParagraphBlock ).toBeTruthy(); - const emptyParagraphBlockElement = - await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - expect( emptyParagraphBlockElement ).toBeTruthy(); await editorPage.sendTextToParagraphBlock( 1, testData.mediumText ); const html = await editorPage.getHtmlContent(); @@ -85,7 +87,9 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - await paragraphBlockElement.click(); + if ( isAndroid() ) { + await paragraphBlockElement.click(); + } await editorPage.removeBlock(); } } ); diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index 7618a4321b93d7..c250653eb8bffd 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -201,14 +201,19 @@ class EditorPage { const titleElement = isAndroid() ? 'Post title. Welcome to Gutenberg!, Updates the title.' : 'post-title'; + + if ( options.autoscroll ) { + await swipeDown( this.driver ); + } + const elements = await this.driver.elementsByAccessibilityId( titleElement ); - if ( elements.length === 0 || ! elements[ 0 ].isDisplayed() ) { - if ( options.autoscroll ) { - await swipeDown( this.driver ); - } + if ( + elements.length === 0 || + ! ( await elements[ 0 ].isDisplayed() ) + ) { return await this.getTitleElement( options ); } return elements[ 0 ]; From a952055f7807ec77201a1cb7e4e18dc00f78a60e Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 9 Mar 2023 16:11:18 +0100 Subject: [PATCH 08/48] Mobile - Block List - Update isFloatingToolbarVisible logic --- .../src/components/block-list/index.native.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index ba0b19c1fc3b9a..da56f789da5cc1 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -213,8 +213,7 @@ export class BlockList extends Component { } = this.props; const { parentScrollRef, onScroll } = extraProps; - const { blockToolbar, blockBorder, headerToolbar, floatingToolbar } = - styles; + const { blockToolbar, headerToolbar, floatingToolbar } = styles; const containerStyle = { flex: isRootList ? 1 : 0, @@ -226,9 +225,9 @@ export class BlockList extends Component { const isContentStretch = contentResizeMode === 'stretch'; const isMultiBlocks = blockClientIds.length > 1; const { isWider } = alignmentHelpers; - const extraScrollHeight = blockToolbar.height + blockBorder.width; - const inputAccessoryViewHeight = + const extraScrollHeight = headerToolbar.height + + blockToolbar.height + ( isFloatingToolbarVisible ? floatingToolbar.height : 0 ); return ( @@ -247,7 +246,6 @@ export class BlockList extends Component { this.scrollViewInnerRef( parentScrollRef || ref ); } } extraScrollHeight={ extraScrollHeight } - inputAccessoryViewHeight={ inputAccessoryViewHeight } keyboardShouldPersistTaps="always" extraData={ this.getExtraData() } scrollEnabled={ isRootList } @@ -372,6 +370,7 @@ export default compose( [ ( select, { rootClientId, orientation, filterInnerBlocks } ) => { const { getBlockCount, + getBlockHierarchyRootClientId, getBlockOrder, getSelectedBlockClientId, isBlockInsertionPointVisible, @@ -392,10 +391,12 @@ export default compose( [ const isReadOnly = getSettings().readOnly; const blockCount = getBlockCount(); - const hasRootInnerBlocks = !! blockCount; + const rootBlockId = getBlockHierarchyRootClientId( + selectedBlockClientId + ); const isFloatingToolbarVisible = - !! selectedBlockClientId && hasRootInnerBlocks; + !! selectedBlockClientId && !! getBlockCount( rootBlockId ); const isRTL = getSettings().isRTL; return { From e2c9d047fa920b8069436f47b435281c6692d2f5 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 9 Mar 2023 16:12:55 +0100 Subject: [PATCH 09/48] Mobile - KeyboardAwareFlatList - Remove usage of inputAccessoryViewHeight --- .../src/mobile/keyboard-aware-flat-list/index.ios.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index f9cea7a761eb92..87ed41daef15f9 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -19,7 +19,6 @@ const AnimatedFlatList = Animated.createAnimatedComponent( FlatList ); export const KeyboardAwareFlatList = ( { extraScrollHeight, innerRef, - inputAccessoryViewHeight, onScroll, scrollEnabled, shouldPreventAutomaticScroll, @@ -35,8 +34,7 @@ export const KeyboardAwareFlatList = ( { const latestContentOffsetY = useSharedValue( -1 ); - const offsetExtraSpace = extraScrollHeight + inputAccessoryViewHeight; - const screenOffset = windowHeight - ( keyboardSpace + offsetExtraSpace ); + const screenOffset = windowHeight - ( keyboardSpace + extraScrollHeight ); const scrollHandler = useAnimatedScrollHandler( { onScroll: ( event ) => { @@ -61,7 +59,7 @@ export const KeyboardAwareFlatList = ( { showSubscription = Keyboard.addListener( 'keyboardDidShow', ( { endCoordinates } ) => { - if ( keyboardSpace === 0 && endCoordinates.height !== 0 ) { + if ( keyboardSpace !== endCoordinates.height ) { setKeyboardSpace( endCoordinates.height ); } } @@ -163,6 +161,7 @@ export const KeyboardAwareFlatList = ( { { ...props } automaticallyAdjustContentInsets={ false } contentInset={ contentInset } + onContentSizeChange={ onScrollToInput } onScroll={ scrollHandler } ref={ getRef } scrollEnabled={ scrollEnabled } From d5b537ffcf4d521fb4f9a964852c985c8cef595a Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 10 Mar 2023 13:24:33 +0100 Subject: [PATCH 10/48] Mobile - KeyboardAwareFlatList - Prevent adding listeners on re-renders and prevent scrolling if the scroll reference doesn't exist --- .../keyboard-aware-flat-list/index.ios.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 87ed41daef15f9..e0143a29ded847 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -59,9 +59,7 @@ export const KeyboardAwareFlatList = ( { showSubscription = Keyboard.addListener( 'keyboardDidShow', ( { endCoordinates } ) => { - if ( keyboardSpace !== endCoordinates.height ) { - setKeyboardSpace( endCoordinates.height ); - } + setKeyboardSpace( endCoordinates.height ); } ); hideSubscription = Keyboard.addListener( 'keyboardWillHide', () => { @@ -76,13 +74,13 @@ export const KeyboardAwareFlatList = ( { showSubscription?.remove(); hideSubscription?.remove(); }; - }, [ scrollEnabled, keyboardSpace, isKeyboardVisible ] ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [] ); const onScrollToInput = useCallback( () => { if ( ! isEditingText.current || ! scrollEnabled || - ! listRef.current || ( isKeyboardVisible.current && keyboardSpace === 0 ) ) { return; @@ -99,7 +97,11 @@ export const KeyboardAwareFlatList = ( { latestContentOffsetY.value + ( textInputOffset - screenOffset ); - if ( offset > 0 && textInputOffset > screenOffset ) { + if ( + listRef.current && + offset > 0 && + textInputOffset > screenOffset + ) { listRef.current.scrollToOffset( { x: 0, offset, @@ -108,7 +110,13 @@ export const KeyboardAwareFlatList = ( { } } ); } - }, [ scrollEnabled, screenOffset, keyboardSpace, latestContentOffsetY ] ); + }, [ + scrollEnabled, + isEditingText, + screenOffset, + keyboardSpace, + latestContentOffsetY, + ] ); useEffect( () => { if ( keyboardSpace !== 0 ) { From d3b28587c4acd58652a9c83700fcbe18a2b55c4d Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 10 Mar 2023 15:57:36 +0100 Subject: [PATCH 11/48] Mobile - E2E - Improve editor paragraph test --- .../__device-tests__/gutenberg-editor-paragraph.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js index 6604c9ba6f32b7..1d89c322173c05 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js @@ -86,6 +86,11 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { await editorPage.sendTextToParagraphBlock( 1, testData.longText ); for ( let i = 3; i > 0; i-- ) { + const paragraphBlockElement = + await editorPage.getTextBlockAtPosition( blockNames.paragraph ); + if ( isAndroid() ) { + await paragraphBlockElement.click(); + } await editorPage.removeBlock(); } } ); From fdc91ef1d395706f7c2a7574e120c032aac86b33 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 16 Mar 2023 12:51:27 +0100 Subject: [PATCH 12/48] Mobile - KeyboardAwareFlatList - Refactors the code to split it in several hooks, it also fixes a few cases that weren't working correctly. --- .../keyboard-aware-flat-list/index.ios.js | 173 ++++++------------ .../use-keyboard-offset.native.js | 59 ++++++ .../use-scroll-to-text-input.native.js | 97 ++++++++++ .../use-text-input-caret-position.native.js | 36 ++++ .../use-text-input-offset.native.js | 40 ++++ 5 files changed, 289 insertions(+), 116 deletions(-) create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index e0143a29ded847..f680398eb895a9 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -2,7 +2,7 @@ * External dependencies */ -import { FlatList, Keyboard, useWindowDimensions } from 'react-native'; +import { ScrollView, FlatList } from 'react-native'; import Animated, { useAnimatedScrollHandler, useSharedValue, @@ -11,10 +11,16 @@ import Animated, { /** * WordPress dependencies */ -import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; -import RCTAztecView from '@wordpress/react-native-aztec'; +import { useCallback, useEffect, useRef } from '@wordpress/element'; -const AnimatedFlatList = Animated.createAnimatedComponent( FlatList ); +/** + * Internal dependencies + */ +import useTextInputOffset from './use-text-input-offset'; +import useKeyboardOffset from './use-keyboard-offset'; +import useScrollToTextInput from './use-scroll-to-text-input'; + +const AnimatedScrollView = Animated.createAnimatedComponent( ScrollView ); export const KeyboardAwareFlatList = ( { extraScrollHeight, @@ -24,17 +30,25 @@ export const KeyboardAwareFlatList = ( { shouldPreventAutomaticScroll, ...props } ) => { - const [ keyboardSpace, setKeyboardSpace ] = useState( 0 ); - const { height: windowHeight } = useWindowDimensions(); - const listRef = useRef(); - const isEditingText = useRef( RCTAztecView.InputState.isFocused() ); - const isKeyboardVisible = useRef( false ); - const currentCaretYCoordinate = useRef( null ); - + const scrollViewMeasurements = useRef(); const latestContentOffsetY = useSharedValue( -1 ); - const screenOffset = windowHeight - ( keyboardSpace + extraScrollHeight ); + const [ isKeyboardVisible, keyboardOffset ] = + useKeyboardOffset( scrollEnabled ); + + const [ textInputOffset ] = useTextInputOffset( scrollEnabled ); + + const [ scrollToTextInputOffset ] = useScrollToTextInput( + extraScrollHeight, + isKeyboardVisible, + keyboardOffset, + latestContentOffsetY, + listRef, + scrollEnabled, + scrollViewMeasurements, + textInputOffset + ); const scrollHandler = useAnimatedScrollHandler( { onScroll: ( event ) => { @@ -45,114 +59,36 @@ export const KeyboardAwareFlatList = ( { } ); useEffect( () => { - let willShowSubscription; - let showSubscription; - let hideSubscription; - - if ( scrollEnabled ) { - willShowSubscription = Keyboard.addListener( - 'keyboardWillShow', - () => { - isKeyboardVisible.current = true; - } - ); - showSubscription = Keyboard.addListener( - 'keyboardDidShow', - ( { endCoordinates } ) => { - setKeyboardSpace( endCoordinates.height ); - } - ); - hideSubscription = Keyboard.addListener( 'keyboardWillHide', () => { - if ( ! RCTAztecView.InputState.isFocused() ) { - setKeyboardSpace( 0 ); - } - isKeyboardVisible.current = false; - } ); - } - return () => { - willShowSubscription?.remove(); - showSubscription?.remove(); - hideSubscription?.remove(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); - - const onScrollToInput = useCallback( () => { + // If the Keyboard is visible it also checks that the keyboard's offset + // is not 0 since the value is updated when the Keyboard is fully visible. if ( - ! isEditingText.current || - ! scrollEnabled || - ( isKeyboardVisible.current && keyboardSpace === 0 ) + ( isKeyboardVisible && keyboardOffset !== 0 ) || + textInputOffset ) { - return; - } - - const textInput = RCTAztecView.InputState.getCurrentFocusedElement(); - if ( textInput ) { - textInput.measureInWindow( ( _x, y, _width, height ) => { - const caretYPosition = - currentCaretYCoordinate.current || height; - const textInputOffset = y + caretYPosition; - - const offset = - latestContentOffsetY.value + - ( textInputOffset - screenOffset ); - - if ( - listRef.current && - offset > 0 && - textInputOffset > screenOffset - ) { - listRef.current.scrollToOffset( { - x: 0, - offset, - animated: true, - } ); - } - } ); + scrollToTextInputOffset(); } }, [ - scrollEnabled, - isEditingText, - screenOffset, - keyboardSpace, - latestContentOffsetY, + isKeyboardVisible, + keyboardOffset, + textInputOffset, + scrollToTextInputOffset, ] ); - useEffect( () => { - if ( keyboardSpace !== 0 ) { - onScrollToInput(); - } - }, [ keyboardSpace, onScrollToInput ] ); - - const onCaretChange = useCallback( - ( { caretY } ) => { - const isFocused = - !! RCTAztecView.InputState.getCurrentFocusedElement(); - isEditingText.current = isFocused; + const measureScrollView = useCallback( () => { + if ( listRef.current && ! scrollViewMeasurements.current ) { + const scrollRef = listRef.current.getNativeScrollRef(); - if ( ! isFocused ) { - return; - } - - currentCaretYCoordinate.current = caretY; - onScrollToInput(); - }, - [ onScrollToInput ] - ); - - useEffect( () => { - if ( scrollEnabled ) { - RCTAztecView.InputState.addCaretChangeListener( onCaretChange ); + scrollRef.measureInWindow( ( _x, y, _width, height ) => { + scrollViewMeasurements.current = { y, height }; + } ); } + }, [] ); - return () => { - if ( scrollEnabled ) { - RCTAztecView.InputState.removeCaretChangeListener( - onCaretChange - ); - } - }; - }, [ scrollEnabled, onCaretChange ] ); + const onContentSizeChange = useCallback( () => { + // Measures the ScrollView to get the Y coordinate and height values. + measureScrollView(); + scrollToTextInputOffset(); + }, [ scrollToTextInputOffset, measureScrollView ] ); const getRef = useCallback( ( ref ) => { @@ -162,18 +98,23 @@ export const KeyboardAwareFlatList = ( { [ innerRef ] ); - const contentInset = { bottom: keyboardSpace }; + // Adds content insets when the keyboard is opened to have + // extra padding at the bottom. + const contentInset = { bottom: keyboardOffset }; return ( - + scrollEventThrottle={ 16 } + > + + ); }; diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js new file mode 100644 index 00000000000000..aa11cdd18c7412 --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js @@ -0,0 +1,59 @@ +/** + * External dependencies + */ + +import { Keyboard } from 'react-native'; + +/** + * WordPress dependencies + */ +import RCTAztecView from '@wordpress/react-native-aztec'; +import { useEffect, useState } from '@wordpress/element'; + +/** + * Hook that adds Keyboard listeners to get the offset space + * when the keyboard is opened, taking into account focused AztecViews. + * + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @return {[boolean, number]} Keyboard visibility state and Keyboard offset. + */ +export default function useKeyboardOffset( scrollEnabled ) { + const [ keyboardOffset, setKeyboardOffset ] = useState( 0 ); + const [ isKeyboardVisible, setIsKeyboardVisible ] = useState( false ); + + useEffect( () => { + let willShowSubscription; + let showSubscription; + let hideSubscription; + + if ( scrollEnabled ) { + willShowSubscription = Keyboard.addListener( + 'keyboardWillShow', + () => { + setIsKeyboardVisible( true ); + } + ); + showSubscription = Keyboard.addListener( + 'keyboardDidShow', + ( { endCoordinates } ) => { + setKeyboardOffset( endCoordinates.height ); + } + ); + hideSubscription = Keyboard.addListener( 'keyboardWillHide', () => { + // Changing focus between TextInputs triggers this listener as the + // Keyboard gets dimissed and then shows up again, so it's needed to + // avoid setting the keyboard offset to 0 unless there's no focused input. + if ( ! RCTAztecView.InputState.isFocused() ) { + setKeyboardOffset( 0 ); + } + setIsKeyboardVisible( false ); + } ); + } + return () => { + willShowSubscription?.remove(); + showSubscription?.remove(); + hideSubscription?.remove(); + }; + }, [ scrollEnabled ] ); + return [ isKeyboardVisible, keyboardOffset ]; +} diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js new file mode 100644 index 00000000000000..f46607cc71ff78 --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js @@ -0,0 +1,97 @@ +/** + * External dependencies + */ + +import { useWindowDimensions } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +/** + * WordPress dependencies + */ +import { useCallback } from '@wordpress/element'; + +/** @typedef {import('@wordpress/element').RefObject} RefObject */ +/** @typedef {import('react-native-reanimated').SharedValuet} SharedValuet */ +/** + * Hook to scroll to the currently focused TextInput + * depending on where the caret is placed taking into + * account the Keyboard and the Header. + * + * @param {number} extraScrollHeight Extra space to not overlap the content. + * @param {boolean} isKeyboardVisible Whether the Keyboard is visible or not. + * @param {number} keyboardOffset Keyboard space offset. + * @param {SharedValuet} latestContentOffsetY Current offset position of the ScrollView. + * @param {RefObject} listRef ScrollView reference. + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {RefObject} scrollViewMeasurements ScrollView component's measurements. + * @param {number} textInputOffset Currently focused TextInput offset. + * @return {Function[]} Function to scroll to the current TextInput's offset. + */ +export default function useScrollToTextInput( + extraScrollHeight, + isKeyboardVisible, + keyboardOffset, + latestContentOffsetY, + listRef, + scrollEnabled, + scrollViewMeasurements, + textInputOffset +) { + const { height: windowHeight } = useWindowDimensions(); + const { top, bottom } = useSafeAreaInsets(); + const availableScreenOffset = Math.round( + windowHeight - ( top + bottom ) - ( keyboardOffset + extraScrollHeight ) + ); + + const shouldScrollUp = useCallback( () => { + return ( + listRef.current && + textInputOffset < scrollViewMeasurements.current.y + ); + }, [ listRef, scrollViewMeasurements, textInputOffset ] ); + + const shouldScrollDown = useCallback( () => { + return listRef.current && textInputOffset >= availableScreenOffset; + }, [ listRef, textInputOffset, availableScreenOffset ] ); + + const scrollToTextInputOffset = useCallback( () => { + if ( + ! scrollEnabled || + ! scrollViewMeasurements.current || + ( isKeyboardVisible && keyboardOffset === 0 ) + ) { + return; + } + + if ( shouldScrollUp() ) { + listRef.current.scrollTo( { + y: latestContentOffsetY.value - textInputOffset, + animated: true, + } ); + return; + } + + if ( shouldScrollDown() ) { + const scrollDownOffset = + latestContentOffsetY.value + + ( textInputOffset - availableScreenOffset ); + listRef.current.scrollTo( { + y: scrollDownOffset, + animated: true, + } ); + } + }, [ + availableScreenOffset, + isKeyboardVisible, + keyboardOffset, + latestContentOffsetY, + listRef, + scrollEnabled, + scrollViewMeasurements, + shouldScrollDown, + shouldScrollUp, + textInputOffset, + ] ); + + return [ scrollToTextInputOffset ]; +} diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js new file mode 100644 index 00000000000000..e39f9d29e915f4 --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js @@ -0,0 +1,36 @@ +/** + * WordPress dependencies + */ +import RCTAztecView from '@wordpress/react-native-aztec'; +import { useCallback, useEffect, useState } from '@wordpress/element'; + +/** + * Hook that listens to caret changes from AztecView TextInputs. + * + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @return {[number]} Current caret's Y coordinate position. + */ +export default function useTextInputCaretPosition( scrollEnabled ) { + const [ currentCaretYPosition, setCurrentCaretYPosition ] = useState(); + + const onCaretChange = useCallback( ( { caretY } ) => { + setCurrentCaretYPosition( caretY ); + }, [] ); + + useEffect( () => { + if ( scrollEnabled ) { + RCTAztecView.InputState.addCaretChangeListener( onCaretChange ); + } else { + RCTAztecView.InputState.removeCaretChangeListener( onCaretChange ); + } + + return () => { + if ( scrollEnabled ) { + RCTAztecView.InputState.removeCaretChangeListener( + onCaretChange + ); + } + }; + }, [ scrollEnabled, onCaretChange ] ); + return [ currentCaretYPosition ]; +} diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js new file mode 100644 index 00000000000000..d92d14eefc74f4 --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js @@ -0,0 +1,40 @@ +/** + * WordPress dependencies + */ +import RCTAztecView from '@wordpress/react-native-aztec'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import useTextInputCaretPosition from './use-text-input-caret-position'; + +/** + * Hook that calculates the currently focused TextInput's current + * caret Y coordinate position. + * + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @return {[number]} Currently focused TextInput's offset. + */ +export default function useTextInputOffset( scrollEnabled ) { + const [ textInputOffset, setTextInputOffset ] = useState(); + + const [ currentCaretYPosition ] = + useTextInputCaretPosition( scrollEnabled ); + + const textInput = RCTAztecView.InputState.getCurrentFocusedElement(); + + if ( textInput && scrollEnabled ) { + textInput.measureInWindow( ( _x, y, _width, height ) => { + const caretYOffset = + // For cases when the focus is at the bottom of the TextInput + // The caretY value is -1 so we use the y + height value. + currentCaretYPosition >= 0 && currentCaretYPosition < height + ? y + currentCaretYPosition + : y + height; + + setTextInputOffset( Math.round( Math.abs( caretYOffset ) ) ); + } ); + } + return [ textInputOffset ]; +} From 5a6c6de55e4734eb94b03fb5357c2ba04d6205e1 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 16 Mar 2023 14:24:38 +0100 Subject: [PATCH 13/48] Mobile - useScrollToTextInput - Fix typo in type --- .../use-scroll-to-text-input.native.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js index f46607cc71ff78..a059b856fac154 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js @@ -11,20 +11,20 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useCallback } from '@wordpress/element'; /** @typedef {import('@wordpress/element').RefObject} RefObject */ -/** @typedef {import('react-native-reanimated').SharedValuet} SharedValuet */ +/** @typedef {import('react-native-reanimated').SharedValue} SharedValue */ /** * Hook to scroll to the currently focused TextInput * depending on where the caret is placed taking into * account the Keyboard and the Header. * - * @param {number} extraScrollHeight Extra space to not overlap the content. - * @param {boolean} isKeyboardVisible Whether the Keyboard is visible or not. - * @param {number} keyboardOffset Keyboard space offset. - * @param {SharedValuet} latestContentOffsetY Current offset position of the ScrollView. - * @param {RefObject} listRef ScrollView reference. - * @param {boolean} scrollEnabled Whether the scroll is enabled or not. - * @param {RefObject} scrollViewMeasurements ScrollView component's measurements. - * @param {number} textInputOffset Currently focused TextInput offset. + * @param {number} extraScrollHeight Extra space to not overlap the content. + * @param {boolean} isKeyboardVisible Whether the Keyboard is visible or not. + * @param {number} keyboardOffset Keyboard space offset. + * @param {SharedValue} latestContentOffsetY Current offset position of the ScrollView. + * @param {RefObject} listRef ScrollView reference. + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {RefObject} scrollViewMeasurements ScrollView component's measurements. + * @param {number} textInputOffset Currently focused TextInput offset. * @return {Function[]} Function to scroll to the current TextInput's offset. */ export default function useScrollToTextInput( From 79cf5580c21c02f7fee54b2a16da0b2846ceda0f Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 21 Mar 2023 16:27:26 +0100 Subject: [PATCH 14/48] Mobile - RCTAztecView - Send caret's height along with its coords --- packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift index 166d86834bc131..c66bbb9c9b03e9 100644 --- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift +++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift @@ -479,6 +479,7 @@ class RCTAztecView: Aztec.TextView { if !(caretEndRect.isInfinite || caretEndRect.isNull) { result["selectionEndCaretX"] = caretEndRect.origin.x result["selectionEndCaretY"] = caretEndRect.origin.y + result["selectionEndCaretHeight"] = caretEndRect.size.height } } From 39611fbe351bfcecc9cbe3a82baf57d4e2ba902e Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 21 Mar 2023 16:28:53 +0100 Subject: [PATCH 15/48] Mobile - Keyboard Aware Flat list - Rewrite hooks to take into account the measurement of the TextInput to its parent, use the caret's height to add extra padding and correct calculation of the top and bottom offsets --- .../keyboard-aware-flat-list/index.ios.js | 40 ++++------- .../use-scroll-to-text-input.native.js | 71 +++++++++++-------- .../use-text-input-caret-position.native.js | 10 +-- .../use-text-input-offset.native.js | 43 +++++------ .../react-native-aztec/src/AztecInputState.js | 22 +++--- packages/react-native-aztec/src/AztecView.js | 5 +- 6 files changed, 98 insertions(+), 93 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index f680398eb895a9..97f9584e3ca52c 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -19,6 +19,7 @@ import { useCallback, useEffect, useRef } from '@wordpress/element'; import useTextInputOffset from './use-text-input-offset'; import useKeyboardOffset from './use-keyboard-offset'; import useScrollToTextInput from './use-scroll-to-text-input'; +import useTextInputCaretPosition from './use-text-input-caret-position'; const AnimatedScrollView = Animated.createAnimatedComponent( ScrollView ); @@ -30,30 +31,34 @@ export const KeyboardAwareFlatList = ( { shouldPreventAutomaticScroll, ...props } ) => { - const listRef = useRef(); - const scrollViewMeasurements = useRef(); - const latestContentOffsetY = useSharedValue( -1 ); + const scrollViewRef = useRef(); + const scrollViewYOffset = useSharedValue( -1 ); const [ isKeyboardVisible, keyboardOffset ] = useKeyboardOffset( scrollEnabled ); - const [ textInputOffset ] = useTextInputOffset( scrollEnabled ); + const [ currentCaretData ] = useTextInputCaretPosition( scrollEnabled ); + const [ textInputOffset ] = useTextInputOffset( + currentCaretData, + scrollEnabled, + scrollViewRef + ); const [ scrollToTextInputOffset ] = useScrollToTextInput( + currentCaretData, extraScrollHeight, isKeyboardVisible, keyboardOffset, - latestContentOffsetY, - listRef, scrollEnabled, - scrollViewMeasurements, + scrollViewRef, + scrollViewYOffset, textInputOffset ); const scrollHandler = useAnimatedScrollHandler( { onScroll: ( event ) => { const { contentOffset } = event; - latestContentOffsetY.value = contentOffset.y; + scrollViewYOffset.value = contentOffset.y; onScroll( event ); }, } ); @@ -74,25 +79,9 @@ export const KeyboardAwareFlatList = ( { scrollToTextInputOffset, ] ); - const measureScrollView = useCallback( () => { - if ( listRef.current && ! scrollViewMeasurements.current ) { - const scrollRef = listRef.current.getNativeScrollRef(); - - scrollRef.measureInWindow( ( _x, y, _width, height ) => { - scrollViewMeasurements.current = { y, height }; - } ); - } - }, [] ); - - const onContentSizeChange = useCallback( () => { - // Measures the ScrollView to get the Y coordinate and height values. - measureScrollView(); - scrollToTextInputOffset(); - }, [ scrollToTextInputOffset, measureScrollView ] ); - const getRef = useCallback( ( ref ) => { - listRef.current = ref; + scrollViewRef.current = ref; innerRef( ref ); }, [ innerRef ] @@ -107,7 +96,6 @@ export const KeyboardAwareFlatList = ( { automaticallyAdjustContentInsets={ false } contentInset={ contentInset } keyboardShouldPersistTaps="handled" - onContentSizeChange={ onContentSizeChange } onScroll={ scrollHandler } ref={ getRef } scrollEnabled={ scrollEnabled } diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js index a059b856fac154..ccf6807714b4ad 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js @@ -3,13 +3,14 @@ */ import { useWindowDimensions } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; /** * WordPress dependencies */ import { useCallback } from '@wordpress/element'; +const DEFAULT_FONT_SIZE = 16; + /** @typedef {import('@wordpress/element').RefObject} RefObject */ /** @typedef {import('react-native-reanimated').SharedValue} SharedValue */ /** @@ -17,55 +18,62 @@ import { useCallback } from '@wordpress/element'; * depending on where the caret is placed taking into * account the Keyboard and the Header. * - * @param {number} extraScrollHeight Extra space to not overlap the content. - * @param {boolean} isKeyboardVisible Whether the Keyboard is visible or not. - * @param {number} keyboardOffset Keyboard space offset. - * @param {SharedValue} latestContentOffsetY Current offset position of the ScrollView. - * @param {RefObject} listRef ScrollView reference. - * @param {boolean} scrollEnabled Whether the scroll is enabled or not. - * @param {RefObject} scrollViewMeasurements ScrollView component's measurements. - * @param {number} textInputOffset Currently focused TextInput offset. + * @param {Object} currentCaretData Current caret's data. + * @param {number} extraScrollHeight Extra space to not overlap the content. + * @param {boolean} isKeyboardVisible Whether the Keyboard is visible or not. + * @param {number} keyboardOffset Keyboard space offset. + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {RefObject} scrollViewRef ScrollView reference. + * @param {SharedValue} scrollViewYOffset Current offset position of the ScrollView. + * @param {number} textInputOffset Currently focused TextInput offset. * @return {Function[]} Function to scroll to the current TextInput's offset. */ export default function useScrollToTextInput( + currentCaretData, extraScrollHeight, isKeyboardVisible, keyboardOffset, - latestContentOffsetY, - listRef, scrollEnabled, - scrollViewMeasurements, + scrollViewRef, + scrollViewYOffset, textInputOffset ) { const { height: windowHeight } = useWindowDimensions(); - const { top, bottom } = useSafeAreaInsets(); + const { caretHeight = DEFAULT_FONT_SIZE } = currentCaretData ?? {}; const availableScreenOffset = Math.round( - windowHeight - ( top + bottom ) - ( keyboardOffset + extraScrollHeight ) + windowHeight - ( keyboardOffset + extraScrollHeight ) ); + const extraPadding = caretHeight * 2; const shouldScrollUp = useCallback( () => { - return ( - listRef.current && - textInputOffset < scrollViewMeasurements.current.y - ); - }, [ listRef, scrollViewMeasurements, textInputOffset ] ); + const offset = textInputOffset - caretHeight; + return offset < scrollViewYOffset.value; + }, [ caretHeight, scrollViewYOffset, textInputOffset ] ); const shouldScrollDown = useCallback( () => { - return listRef.current && textInputOffset >= availableScreenOffset; - }, [ listRef, textInputOffset, availableScreenOffset ] ); + const offset = + scrollViewYOffset.value + availableScreenOffset - extraPadding; + return textInputOffset > offset; + }, [ + availableScreenOffset, + extraPadding, + scrollViewYOffset, + textInputOffset, + ] ); const scrollToTextInputOffset = useCallback( () => { if ( + ! scrollViewRef.current || ! scrollEnabled || - ! scrollViewMeasurements.current || ( isKeyboardVisible && keyboardOffset === 0 ) ) { return; } if ( shouldScrollUp() ) { - listRef.current.scrollTo( { - y: latestContentOffsetY.value - textInputOffset, + const scrollUpOffset = scrollViewYOffset.value - extraPadding; + scrollViewRef.current.scrollTo( { + y: scrollUpOffset, animated: true, } ); return; @@ -73,21 +81,24 @@ export default function useScrollToTextInput( if ( shouldScrollDown() ) { const scrollDownOffset = - latestContentOffsetY.value + - ( textInputOffset - availableScreenOffset ); - listRef.current.scrollTo( { + textInputOffset + + extraScrollHeight - + availableScreenOffset + + extraPadding; + scrollViewRef.current.scrollTo( { y: scrollDownOffset, animated: true, } ); } }, [ availableScreenOffset, + extraPadding, + extraScrollHeight, isKeyboardVisible, keyboardOffset, - latestContentOffsetY, - listRef, scrollEnabled, - scrollViewMeasurements, + scrollViewRef, + scrollViewYOffset, shouldScrollDown, shouldScrollUp, textInputOffset, diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js index e39f9d29e915f4..49aa873fc66efc 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-caret-position.native.js @@ -8,13 +8,13 @@ import { useCallback, useEffect, useState } from '@wordpress/element'; * Hook that listens to caret changes from AztecView TextInputs. * * @param {boolean} scrollEnabled Whether the scroll is enabled or not. - * @return {[number]} Current caret's Y coordinate position. + * @return {[number]} Current caret's data. */ export default function useTextInputCaretPosition( scrollEnabled ) { - const [ currentCaretYPosition, setCurrentCaretYPosition ] = useState(); + const [ currentCaretData, setCurrentCaretData ] = useState(); - const onCaretChange = useCallback( ( { caretY } ) => { - setCurrentCaretYPosition( caretY ); + const onCaretChange = useCallback( ( caret ) => { + setCurrentCaretData( caret ); }, [] ); useEffect( () => { @@ -32,5 +32,5 @@ export default function useTextInputCaretPosition( scrollEnabled ) { } }; }, [ scrollEnabled, onCaretChange ] ); - return [ currentCaretYPosition ]; + return [ currentCaretData ]; } diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js index d92d14eefc74f4..f1686ac20ecba6 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js @@ -4,37 +4,40 @@ import RCTAztecView from '@wordpress/react-native-aztec'; import { useState } from '@wordpress/element'; -/** - * Internal dependencies - */ -import useTextInputCaretPosition from './use-text-input-caret-position'; - +/** @typedef {import('@wordpress/element').RefObject} RefObject */ /** * Hook that calculates the currently focused TextInput's current * caret Y coordinate position. * - * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {Object} currentCaretData Current caret's data. + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {RefObject} scrollViewRef ScrollView reference. * @return {[number]} Currently focused TextInput's offset. */ -export default function useTextInputOffset( scrollEnabled ) { +export default function useTextInputOffset( + currentCaretData, + scrollEnabled, + scrollViewRef +) { const [ textInputOffset, setTextInputOffset ] = useState(); - const [ currentCaretYPosition ] = - useTextInputCaretPosition( scrollEnabled ); - const textInput = RCTAztecView.InputState.getCurrentFocusedElement(); - if ( textInput && scrollEnabled ) { - textInput.measureInWindow( ( _x, y, _width, height ) => { - const caretYOffset = - // For cases when the focus is at the bottom of the TextInput - // The caretY value is -1 so we use the y + height value. - currentCaretYPosition >= 0 && currentCaretYPosition < height - ? y + currentCaretYPosition - : y + height; + if ( scrollViewRef.current && textInput && scrollEnabled ) { + textInput.measureLayout( + scrollViewRef.current, + ( _x, y, _width, height ) => { + const { caretY = null } = currentCaretData ?? {}; + const caretYOffset = + // For cases when the focus is at the bottom of the TextInput + // The caretY value isr -1 or null so we use the y + height value. + caretY !== null && caretY >= 0 && caretY < height + ? y + caretY + : y + height; - setTextInputOffset( Math.round( Math.abs( caretYOffset ) ) ); - } ); + setTextInputOffset( Math.round( Math.abs( caretYOffset ) ) ); + } + ); } return [ textInputOffset ]; } diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 8ffe376e0e641c..e47d5576224332 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -9,7 +9,7 @@ const focusChangeListeners = []; const caretChangeListeners = []; let currentFocusedElement = null; -let currentCaretYCoordinate = null; +let currentCaretData = null; /** * Adds a listener that will be called in the following cases: @@ -76,7 +76,7 @@ export const removeCaretChangeListener = ( listener ) => { */ const notifyCaretChangeListeners = () => { caretChangeListeners.forEach( ( listener ) => { - listener( { caretY: getCurrentCaretYCoordinate() } ); + listener( getCurrentCaretData() ); } ); }; @@ -133,7 +133,7 @@ export const focus = ( element ) => { */ export const blur = ( element ) => { TextInputState.blurTextInput( element ); - setCurrentCaretYCoordinate( null ); + setCurrentCaretData( null ); notifyInputChange(); }; @@ -147,22 +147,22 @@ export const blurCurrentFocusedElement = () => { }; /** - * Sets the current focused element caret's Y coordinate. + * Sets the current focused element caret's data. * - * @param {?number} yCoordinate Caret's Y coordinate. + * @param {Object} caret Caret's data. */ -export const setCurrentCaretYCoordinate = ( yCoordinate ) => { +export const setCurrentCaretData = ( caret ) => { if ( isFocused() ) { - currentCaretYCoordinate = yCoordinate; + currentCaretData = caret; notifyCaretChangeListeners(); } }; /** - * Get the current focused element caret's Y coordinate. + * Get the current focused element caret's data. * - * @return {?number} Current caret's Y Coordinate. + * @return {Object} Current caret's data. */ -export const getCurrentCaretYCoordinate = () => { - return currentCaretYCoordinate; +export const getCurrentCaretData = () => { + return currentCaretData; }; diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index a81739f34eca17..7505dbf71fd2c6 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -201,7 +201,10 @@ class AztecView extends Component { this.selectionEndCaretY !== event.nativeEvent.selectionEndCaretY ) { const caretY = event.nativeEvent.selectionEndCaretY; - AztecInputState.setCurrentCaretYCoordinate( caretY ); + AztecInputState.setCurrentCaretData( { + caretY, + caretHeight: event.nativeEvent?.selectionEndCaretHeight, + } ); this.selectionEndCaretY = caretY; } } From 4f7651f0c71f59c129a0082b5f1ac3329781877f Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 22 Mar 2023 19:25:50 +0100 Subject: [PATCH 16/48] Mobile - Keyboard Aware Flatlist - Update hooks to fix several bugs --- .../keyboard-aware-flat-list/index.ios.js | 50 ++++--- .../use-scroll-to-text-input.native.js | 122 ++++++++---------- .../use-text-input-offset.native.js | 61 ++++----- 3 files changed, 120 insertions(+), 113 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 97f9584e3ca52c..f49fd83a2db0e5 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -38,47 +38,58 @@ export const KeyboardAwareFlatList = ( { useKeyboardOffset( scrollEnabled ); const [ currentCaretData ] = useTextInputCaretPosition( scrollEnabled ); - const [ textInputOffset ] = useTextInputOffset( - currentCaretData, + + const [ getTextInputOffset ] = useTextInputOffset( scrollEnabled, scrollViewRef ); const [ scrollToTextInputOffset ] = useScrollToTextInput( - currentCaretData, extraScrollHeight, - isKeyboardVisible, keyboardOffset, scrollEnabled, scrollViewRef, - scrollViewYOffset, - textInputOffset + scrollViewYOffset ); - const scrollHandler = useAnimatedScrollHandler( { - onScroll: ( event ) => { - const { contentOffset } = event; - scrollViewYOffset.value = contentOffset.y; - onScroll( event ); + const onScrollToTextInput = useCallback( + async ( caret ) => { + const textInputOffset = await getTextInputOffset( caret ); + + if ( textInputOffset !== null || textInputOffset !== null ) { + scrollToTextInputOffset( caret, textInputOffset ); + } }, - } ); + [ getTextInputOffset, scrollToTextInputOffset ] + ); useEffect( () => { - // If the Keyboard is visible it also checks that the keyboard's offset - // is not 0 since the value is updated when the Keyboard is fully visible. + const caretY = currentCaretData?.caretY; if ( - ( isKeyboardVisible && keyboardOffset !== 0 ) || - textInputOffset + ( isKeyboardVisible && keyboardOffset !== 0 && caretY !== null ) || + caretY !== null ) { - scrollToTextInputOffset(); + onScrollToTextInput( currentCaretData ); } }, [ + currentCaretData, isKeyboardVisible, keyboardOffset, - textInputOffset, - scrollToTextInputOffset, + onScrollToTextInput, ] ); + const scrollHandler = useAnimatedScrollHandler( { + onScroll: ( event ) => { + const { contentOffset } = event; + scrollViewYOffset.value = contentOffset.y; + onScroll( event ); + }, + } ); + + const onContentSizeChange = useCallback( () => { + onScrollToTextInput( currentCaretData ); + }, [ onScrollToTextInput, currentCaretData ] ); + const getRef = useCallback( ( ref ) => { scrollViewRef.current = ref; @@ -96,6 +107,7 @@ export const KeyboardAwareFlatList = ( { automaticallyAdjustContentInsets={ false } contentInset={ contentInset } keyboardShouldPersistTaps="handled" + onContentSizeChange={ onContentSizeChange } onScroll={ scrollHandler } ref={ getRef } scrollEnabled={ scrollEnabled } diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js index ccf6807714b4ad..bd69642c43cbeb 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js @@ -3,7 +3,7 @@ */ import { useWindowDimensions } from 'react-native'; - +import { useSafeAreaInsets } from 'react-native-safe-area-context'; /** * WordPress dependencies */ @@ -18,91 +18,83 @@ const DEFAULT_FONT_SIZE = 16; * depending on where the caret is placed taking into * account the Keyboard and the Header. * - * @param {Object} currentCaretData Current caret's data. * @param {number} extraScrollHeight Extra space to not overlap the content. - * @param {boolean} isKeyboardVisible Whether the Keyboard is visible or not. * @param {number} keyboardOffset Keyboard space offset. * @param {boolean} scrollEnabled Whether the scroll is enabled or not. * @param {RefObject} scrollViewRef ScrollView reference. * @param {SharedValue} scrollViewYOffset Current offset position of the ScrollView. - * @param {number} textInputOffset Currently focused TextInput offset. * @return {Function[]} Function to scroll to the current TextInput's offset. */ export default function useScrollToTextInput( - currentCaretData, extraScrollHeight, - isKeyboardVisible, keyboardOffset, scrollEnabled, scrollViewRef, - scrollViewYOffset, - textInputOffset + scrollViewYOffset ) { const { height: windowHeight } = useWindowDimensions(); - const { caretHeight = DEFAULT_FONT_SIZE } = currentCaretData ?? {}; + const { top, bottom } = useSafeAreaInsets(); const availableScreenOffset = Math.round( - windowHeight - ( keyboardOffset + extraScrollHeight ) + windowHeight - ( top + bottom ) - ( keyboardOffset + extraScrollHeight ) ); - const extraPadding = caretHeight * 2; - const shouldScrollUp = useCallback( () => { - const offset = textInputOffset - caretHeight; - return offset < scrollViewYOffset.value; - }, [ caretHeight, scrollViewYOffset, textInputOffset ] ); + const shouldScrollUp = useCallback( + ( textInputOffset, caretHeight ) => { + const offset = textInputOffset - caretHeight; + return offset < scrollViewYOffset.value; + }, + [ scrollViewYOffset ] + ); - const shouldScrollDown = useCallback( () => { - const offset = - scrollViewYOffset.value + availableScreenOffset - extraPadding; - return textInputOffset > offset; - }, [ - availableScreenOffset, - extraPadding, - scrollViewYOffset, - textInputOffset, - ] ); + const shouldScrollDown = useCallback( + ( textInputOffset, extraPadding ) => { + const offset = + scrollViewYOffset.value + availableScreenOffset - extraPadding; + return textInputOffset > offset; + }, + [ availableScreenOffset, scrollViewYOffset ] + ); - const scrollToTextInputOffset = useCallback( () => { - if ( - ! scrollViewRef.current || - ! scrollEnabled || - ( isKeyboardVisible && keyboardOffset === 0 ) - ) { - return; - } + const scrollToTextInputOffset = useCallback( + ( caret, textInputOffset ) => { + const { caretHeight = DEFAULT_FONT_SIZE } = caret ?? {}; + const extraPadding = caretHeight * 2; - if ( shouldScrollUp() ) { - const scrollUpOffset = scrollViewYOffset.value - extraPadding; - scrollViewRef.current.scrollTo( { - y: scrollUpOffset, - animated: true, - } ); - return; - } + if ( ! scrollViewRef.current || ! scrollEnabled ) { + return; + } - if ( shouldScrollDown() ) { - const scrollDownOffset = - textInputOffset + - extraScrollHeight - - availableScreenOffset + - extraPadding; - scrollViewRef.current.scrollTo( { - y: scrollDownOffset, - animated: true, - } ); - } - }, [ - availableScreenOffset, - extraPadding, - extraScrollHeight, - isKeyboardVisible, - keyboardOffset, - scrollEnabled, - scrollViewRef, - scrollViewYOffset, - shouldScrollDown, - shouldScrollUp, - textInputOffset, - ] ); + if ( shouldScrollUp( textInputOffset, caretHeight ) ) { + const scrollUpOffset = scrollViewYOffset.value - extraPadding; + scrollViewRef.current.scrollTo( { + y: scrollUpOffset, + animated: true, + } ); + return; + } + + if ( shouldScrollDown( textInputOffset, extraPadding ) ) { + const scrollDownOffset = + textInputOffset + + extraScrollHeight - + availableScreenOffset + + extraPadding; + scrollViewRef.current.scrollTo( { + y: scrollDownOffset, + animated: true, + } ); + } + }, + [ + availableScreenOffset, + extraScrollHeight, + scrollEnabled, + scrollViewRef, + scrollViewYOffset, + shouldScrollDown, + shouldScrollUp, + ] + ); return [ scrollToTextInputOffset ]; } diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js index f1686ac20ecba6..e4418baaa70c2c 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js @@ -2,42 +2,45 @@ * WordPress dependencies */ import RCTAztecView from '@wordpress/react-native-aztec'; -import { useState } from '@wordpress/element'; +import { useCallback } from '@wordpress/element'; /** @typedef {import('@wordpress/element').RefObject} RefObject */ /** * Hook that calculates the currently focused TextInput's current * caret Y coordinate position. * - * @param {Object} currentCaretData Current caret's data. - * @param {boolean} scrollEnabled Whether the scroll is enabled or not. - * @param {RefObject} scrollViewRef ScrollView reference. - * @return {[number]} Currently focused TextInput's offset. + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {RefObject} scrollViewRef ScrollView reference. + * @return {[Function]} Function to get the current TextInput's offset. */ -export default function useTextInputOffset( - currentCaretData, - scrollEnabled, - scrollViewRef -) { - const [ textInputOffset, setTextInputOffset ] = useState(); +export default function useTextInputOffset( scrollEnabled, scrollViewRef ) { + const getTextInputOffset = useCallback( + async ( caret ) => { + const { caretY = null } = caret ?? {}; + const textInput = + RCTAztecView.InputState.getCurrentFocusedElement(); - const textInput = RCTAztecView.InputState.getCurrentFocusedElement(); + return new Promise( ( resolve ) => { + if ( scrollViewRef.current && textInput && scrollEnabled ) { + textInput.measureLayout( + scrollViewRef.current, + ( _x, y, _width, height ) => { + const caretYOffset = + // For cases where the caretY value is -1 we use the y + height value. + caretY >= 0 && caretY < height + ? y + caretY + : y + height; + resolve( Math.round( Math.abs( caretYOffset ) ) ); + }, + () => resolve( null ) + ); + } else { + resolve( null ); + } + } ); + }, + [ scrollEnabled, scrollViewRef ] + ); - if ( scrollViewRef.current && textInput && scrollEnabled ) { - textInput.measureLayout( - scrollViewRef.current, - ( _x, y, _width, height ) => { - const { caretY = null } = currentCaretData ?? {}; - const caretYOffset = - // For cases when the focus is at the bottom of the TextInput - // The caretY value isr -1 or null so we use the y + height value. - caretY !== null && caretY >= 0 && caretY < height - ? y + caretY - : y + height; - - setTextInputOffset( Math.round( Math.abs( caretYOffset ) ) ); - } - ); - } - return [ textInputOffset ]; + return [ getTextInputOffset ]; } From a0a0e225770284ae28c55a801d9cbbdea01783b7 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 22 Mar 2023 19:26:13 +0100 Subject: [PATCH 17/48] Mobile - Keyboard Aware Flatlist - Add tests for new hooks --- .../test/use-keyboard-offset.native.js | 69 ++++++++ .../test/use-scroll-to-text-input.native.js | 144 +++++++++++++++++ .../use-text-input-caret-position.native.js | 82 ++++++++++ .../test/use-text-input-offset.native.js | 147 ++++++++++++++++++ 4 files changed, 442 insertions(+) create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/test/use-text-input-caret-position.native.js create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/test/use-text-input-offset.native.js diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js new file mode 100644 index 00000000000000..8d68eb6196adea --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { act, renderHook } from '@testing-library/react-native'; +import { Keyboard } from 'react-native'; +import RCTDeviceEventEmitter from 'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter'; + +/** + * Internal dependencies + */ +import useKeyboardOffset from '../use-keyboard-offset'; + +describe( 'useKeyboardOffset', () => { + beforeEach( () => { + Keyboard.removeAllListeners( 'keyboardWillShow' ); + Keyboard.removeAllListeners( 'keyboardDidShow' ); + Keyboard.removeAllListeners( 'keyboardWillHide' ); + } ); + + it( 'returns the initial state', () => { + // Arrange + const { result } = renderHook( () => useKeyboardOffset( true ) ); + const [ isKeyboardVisible, keyboardOffset ] = result.current; + + // Assert + expect( isKeyboardVisible ).toBe( false ); + expect( keyboardOffset ).toBe( 0 ); + } ); + + it( 'updates keyboard visibility and offset when the keyboard is shown', () => { + // Arrange + const { result } = renderHook( () => useKeyboardOffset( true ) ); + + // Act + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardWillShow' ); + RCTDeviceEventEmitter.emit( 'keyboardDidShow', { + endCoordinates: { height: 250 }, + } ); + } ); + + // Assert + const [ isKeyboardVisible, keyboardOffset ] = result.current; + expect( isKeyboardVisible ).toBe( true ); + expect( keyboardOffset ).toBe( 250 ); + } ); + + it( 'updates keyboard visibility and offset when the keyboard is hidden', () => { + // Arrange + const { result } = renderHook( () => useKeyboardOffset( true ) ); + + // Act + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardWillShow' ); + RCTDeviceEventEmitter.emit( 'keyboardDidShow', { + endCoordinates: { height: 250 }, + } ); + } ); + + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardWillHide' ); + } ); + + // Assert + const [ isKeyboardVisible, keyboardOffset ] = result.current; + expect( isKeyboardVisible ).toBe( false ); + expect( keyboardOffset ).toBe( 0 ); + } ); +} ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js new file mode 100644 index 00000000000000..f9b6202ad05a83 --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js @@ -0,0 +1,144 @@ +/** + * External dependencies + */ + +import { renderHook } from '@testing-library/react-native'; + +/** + * Internal dependencies + */ +import useScrollToTextInput from '../use-scroll-to-text-input'; + +const mockUseWindowDimensions = jest.fn(); +jest.mock( 'react-native/Libraries/Utilities/useWindowDimensions', () => ( { + default: mockUseWindowDimensions, +} ) ); + +describe( 'useScrollToTextInput', () => { + it( 'scrolls up to the TextInput offset when the caret is at the top of the screen', () => { + // Arrange + const currentCaretData = { caretHeight: 10 }; + const extraScrollHeight = 50; + const keyboardOffset = 100; + const scrollEnabled = true; + const scrollViewRef = { current: { scrollTo: jest.fn() } }; + const scrollViewYOffset = { value: 150 }; + const textInputOffset = 50; + const windowHeight = 600; + mockUseWindowDimensions.mockReturnValue( { height: windowHeight } ); + + const { result } = renderHook( () => + useScrollToTextInput( + extraScrollHeight, + keyboardOffset, + scrollEnabled, + scrollViewRef, + scrollViewYOffset + ) + ); + + // Act + result.current[ 0 ]( currentCaretData, textInputOffset ); + + // Assert + expect( scrollViewRef.current.scrollTo ).toHaveBeenCalledWith( { + y: scrollViewYOffset.value - currentCaretData.caretHeight * 2, + animated: true, + } ); + } ); + + it( 'scrolls down to the TextInput offset when the caret is at the bottom of the screen', () => { + // Arrange + const currentCaretData = { caretHeight: 10 }; + const extraScrollHeight = 50; + const keyboardOffset = 100; + const scrollEnabled = true; + const scrollViewRef = { current: { scrollTo: jest.fn() } }; + const scrollViewYOffset = { value: 0 }; + const textInputOffset = 550; + const windowHeight = 600; + mockUseWindowDimensions.mockReturnValue( { height: windowHeight } ); + + const { result } = renderHook( () => + useScrollToTextInput( + extraScrollHeight, + keyboardOffset, + scrollEnabled, + scrollViewRef, + scrollViewYOffset + ) + ); + + // Act + result.current[ 0 ]( currentCaretData, textInputOffset ); + + // Assert + const expectedYOffset = + textInputOffset + + extraScrollHeight - + ( windowHeight - ( keyboardOffset + extraScrollHeight ) ) + + currentCaretData.caretHeight * 2; + expect( scrollViewRef.current.scrollTo ).toHaveBeenCalledWith( { + y: expectedYOffset, + animated: true, + } ); + } ); + + it( 'does not scroll when the ScrollView ref is not available', () => { + // Arrange + const currentCaretData = { caretHeight: 10 }; + const extraScrollHeight = 50; + const keyboardOffset = 100; + const scrollEnabled = true; + const scrollViewRef = { current: null }; + const scrollViewYOffset = { value: 0 }; + const textInputOffset = 50; + const windowHeight = 600; + mockUseWindowDimensions.mockReturnValue( { height: windowHeight } ); + + const { result } = renderHook( () => + useScrollToTextInput( + extraScrollHeight, + keyboardOffset, + scrollEnabled, + scrollViewRef, + scrollViewYOffset + ) + ); + + // Act + result.current[ 0 ]( currentCaretData, textInputOffset ); + + // Assert + expect( scrollViewRef.current ).toBeNull(); + } ); + + it( 'does not scroll when the scroll is not enabled', () => { + // Arrange + const currentCaretData = { caretHeight: 10 }; + const extraScrollHeight = 50; + const keyboardOffset = 100; + const scrollEnabled = false; + const scrollViewRef = { current: { scrollTo: jest.fn() } }; + const scrollViewYOffset = { value: 0 }; + const textInputOffset = 50; + const windowHeight = 600; + mockUseWindowDimensions.mockReturnValue( { height: windowHeight } ); + + const { result } = renderHook( () => + useScrollToTextInput( + extraScrollHeight, + keyboardOffset, + scrollEnabled, + scrollViewRef, + scrollViewYOffset + ) + ); + + // Act + result.current[ 0 ]( currentCaretData, textInputOffset ); + + // Assert + expect( scrollViewRef.current.scrollTo ).not.toHaveBeenCalled(); + } ); +} ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-text-input-caret-position.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-text-input-caret-position.native.js new file mode 100644 index 00000000000000..6cb9bd5be81aab --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-text-input-caret-position.native.js @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import { renderHook } from '@testing-library/react-native'; + +/** + * WordPress dependencies + */ +import RCTAztecView from '@wordpress/react-native-aztec'; + +/** + * Internal dependencies + */ +import useTextInputCaretPosition from '../use-text-input-caret-position'; + +describe( 'useTextInputCaretPosition', () => { + let addCaretChangeListenerSpy; + let removeCaretChangeListenerSpy; + + beforeAll( () => { + addCaretChangeListenerSpy = jest.spyOn( + RCTAztecView.InputState, + 'addCaretChangeListener' + ); + removeCaretChangeListenerSpy = jest.spyOn( + RCTAztecView.InputState, + 'removeCaretChangeListener' + ); + } ); + + beforeEach( () => { + addCaretChangeListenerSpy.mockClear(); + removeCaretChangeListenerSpy.mockClear(); + } ); + + it( 'should add and remove caret change listener correctly', () => { + // Arrange + const scrollEnabled = true; + + // Act + const { unmount } = renderHook( () => + useTextInputCaretPosition( scrollEnabled ) + ); + unmount(); + + // Assert + expect( addCaretChangeListenerSpy ).toHaveBeenCalledTimes( 1 ); + expect( removeCaretChangeListenerSpy ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'should add caret change listener when scroll is enabled', () => { + // Arrange + const scrollEnabled = true; + + // Act + renderHook( () => useTextInputCaretPosition( scrollEnabled ) ); + + // Assert + expect( addCaretChangeListenerSpy ).toHaveBeenCalledTimes( 1 ); + expect( removeCaretChangeListenerSpy ).not.toHaveBeenCalled(); + } ); + + it( 'should remove caret change listener when scroll is enabled and then changed to disabled', () => { + // Arrange + const { rerender } = renderHook( + ( props ) => useTextInputCaretPosition( props.scrollEnabled ), + { + initialProps: { scrollEnabled: true }, + } + ); + + // Assert + expect( addCaretChangeListenerSpy ).toHaveBeenCalled(); + + // Act + rerender( { scrollEnabled: false } ); + + // Assert + expect( removeCaretChangeListenerSpy ).toHaveBeenCalled(); + expect( addCaretChangeListenerSpy ).toHaveBeenCalledTimes( 1 ); + } ); +} ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-text-input-offset.native.js new file mode 100644 index 00000000000000..850b8c09a03b91 --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-text-input-offset.native.js @@ -0,0 +1,147 @@ +/** + * External dependencies + */ +import { renderHook } from '@testing-library/react-native'; + +/** + * WordPress dependencies + */ +import RCTAztecView from '@wordpress/react-native-aztec'; + +/** + * Internal dependencies + */ +import useTextInputOffset from '../use-text-input-offset'; + +jest.mock( '@wordpress/react-native-aztec', () => ( { + InputState: { + getCurrentFocusedElement: jest.fn(), + }, +} ) ); + +describe( 'useTextInputOffset', () => { + afterEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should return a function', () => { + // Arrange + const scrollViewRef = { current: {} }; + const scrollEnabled = true; + + // Act + const { result } = renderHook( () => + useTextInputOffset( scrollEnabled, scrollViewRef ) + ); + + // Assert + expect( result.current[ 0 ] ).toBeInstanceOf( Function ); + } ); + + it( 'should return null when scrollViewRef.current is null', async () => { + // Arrange + const scrollViewRef = { current: null }; + const scrollEnabled = true; + + // Act + const { result } = renderHook( () => + useTextInputOffset( scrollEnabled, scrollViewRef ) + ); + const getTextInputOffset = result.current[ 0 ]; + + // Assert + const offset = await getTextInputOffset(); + expect( offset ).toBeNull(); + } ); + + it( 'should return null when textInput is null', async () => { + // Arrange + const scrollViewRef = { current: {} }; + const scrollEnabled = true; + RCTAztecView.InputState.getCurrentFocusedElement.mockReturnValue( + null + ); + + // Act + const { result } = renderHook( () => + useTextInputOffset( scrollEnabled, scrollViewRef ) + ); + const getTextInputOffset = result.current[ 0 ]; + + // Assert + const offset = await getTextInputOffset(); + expect( offset ).toBeNull(); + } ); + + it( 'should return null when scroll is not enabled', async () => { + // Arrange + const scrollViewRef = { current: {} }; + const scrollEnabled = false; + + // Act + const { result } = renderHook( () => + useTextInputOffset( scrollEnabled, scrollViewRef ) + ); + const getTextInputOffset = result.current[ 0 ]; + + // Assert + const offset = await getTextInputOffset(); + expect( offset ).toBeNull(); + } ); + + it( 'should return correct offset value when caretY is not null', async () => { + // Arrange + const scrollViewRef = { current: {} }; + const scrollEnabled = true; + const x = 0; + const y = 10; + const width = 0; + const height = 100; + const textInput = { + measureLayout: jest.fn( ( _, callback ) => { + callback( x, y, width, height ); + } ), + }; + RCTAztecView.InputState.getCurrentFocusedElement.mockReturnValue( + textInput + ); + + // Act + const { result } = renderHook( () => + useTextInputOffset( scrollEnabled, scrollViewRef ) + ); + const getTextInputOffset = result.current[ 0 ]; + + // Assert + const offset = await getTextInputOffset( { caretY: 10 } ); + expect( offset ).toBe( 20 ); + } ); + + it( 'should return correct offset value when caretY is -1', async () => { + // Arrange + const scrollViewRef = { current: {} }; + const scrollEnabled = true; + const x = 0; + const y = 10; + const width = 0; + const height = 100; + const textInput = { + measureLayout: jest.fn( ( _, callback ) => { + callback( x, y, width, height ); + } ), + }; + RCTAztecView.InputState.getCurrentFocusedElement.mockReturnValue( + textInput + ); + + // Act + const { result } = renderHook( () => + useTextInputOffset( scrollEnabled, scrollViewRef ) + ); + const getTextInputOffset = result.current[ 0 ]; + + // Assert + const offset = await getTextInputOffset( { caretY: -1 } ); + expect( offset ).toBe( 110 ); + } ); +} ); From a2ccc9197326f2fdee8d2d436d00e9de7535df66 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 23 Mar 2023 14:28:20 +0100 Subject: [PATCH 18/48] Mobile - Keyboard Aware FlatList - Remove duplicated condition and adds comments --- .../src/mobile/keyboard-aware-flat-list/index.ios.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index f49fd83a2db0e5..9436d9d88af068 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -56,7 +56,7 @@ export const KeyboardAwareFlatList = ( { async ( caret ) => { const textInputOffset = await getTextInputOffset( caret ); - if ( textInputOffset !== null || textInputOffset !== null ) { + if ( textInputOffset !== null ) { scrollToTextInputOffset( caret, textInputOffset ); } }, @@ -64,9 +64,14 @@ export const KeyboardAwareFlatList = ( { ); useEffect( () => { + // Waits for the Keyboard to be visible and the Keyboard offset to be set. + const awaitKeyboardOffsetIfVisible = + isKeyboardVisible && keyboardOffset !== 0; const caretY = currentCaretData?.caretY; + if ( - ( isKeyboardVisible && keyboardOffset !== 0 && caretY !== null ) || + // We need to check for cases when the Keyboard is visible or not. + ( awaitKeyboardOffsetIfVisible && caretY !== null ) || caretY !== null ) { onScrollToTextInput( currentCaretData ); From d5d9f52f9f63fa284318705150ee06836e47e56c Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 24 Mar 2023 01:00:44 +0100 Subject: [PATCH 19/48] Mobile - Keyboard Aware Flatlist - Update useScrollToTextInput to fix bugs and simplify logic --- .../keyboard-aware-flat-list/index.ios.js | 86 +++++++++++++------ .../test/use-scroll-to-text-input.native.js | 40 ++++----- .../use-scroll-to-text-input.native.js | 75 ++++++---------- .../use-text-input-offset.native.js | 7 +- 4 files changed, 106 insertions(+), 102 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 9436d9d88af068..a0e6014869bc05 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -2,7 +2,7 @@ * External dependencies */ -import { ScrollView, FlatList } from 'react-native'; +import { ScrollView, FlatList, useWindowDimensions } from 'react-native'; import Animated, { useAnimatedScrollHandler, useSharedValue, @@ -12,6 +12,7 @@ import Animated, { * WordPress dependencies */ import { useCallback, useEffect, useRef } from '@wordpress/element'; +import { useThrottle } from '@wordpress/compose'; /** * Internal dependencies @@ -32,8 +33,12 @@ export const KeyboardAwareFlatList = ( { ...props } ) => { const scrollViewRef = useRef(); + const scrollViewMeasurements = useRef(); const scrollViewYOffset = useSharedValue( -1 ); + const { height: windowHeight, width: windowWidth } = useWindowDimensions(); + const isLandscape = windowWidth >= windowHeight; + const [ isKeyboardVisible, keyboardOffset ] = useKeyboardOffset( scrollEnabled ); @@ -48,40 +53,50 @@ export const KeyboardAwareFlatList = ( { extraScrollHeight, keyboardOffset, scrollEnabled, + scrollViewMeasurements, scrollViewRef, scrollViewYOffset ); - const onScrollToTextInput = useCallback( - async ( caret ) => { - const textInputOffset = await getTextInputOffset( caret ); - - if ( textInputOffset !== null ) { - scrollToTextInputOffset( caret, textInputOffset ); - } - }, - [ getTextInputOffset, scrollToTextInputOffset ] + const onScrollToTextInput = useThrottle( + useCallback( + async ( caret ) => { + const textInputOffset = await getTextInputOffset( caret ); + const isKeyboardVisibleWithOffset = + isKeyboardVisible && keyboardOffset !== 0; + const hasTextInputOffset = textInputOffset !== null; + + if ( + ( isKeyboardVisibleWithOffset && hasTextInputOffset ) || + ( ! isKeyboardVisible && hasTextInputOffset ) + ) { + scrollToTextInputOffset( caret, textInputOffset ); + } + }, + [ + getTextInputOffset, + isKeyboardVisible, + keyboardOffset, + scrollToTextInputOffset, + ] + ), + 200, + { leading: false } ); useEffect( () => { - // Waits for the Keyboard to be visible and the Keyboard offset to be set. - const awaitKeyboardOffsetIfVisible = - isKeyboardVisible && keyboardOffset !== 0; - const caretY = currentCaretData?.caretY; - - if ( - // We need to check for cases when the Keyboard is visible or not. - ( awaitKeyboardOffsetIfVisible && caretY !== null ) || - caretY !== null - ) { - onScrollToTextInput( currentCaretData ); + onScrollToTextInput( currentCaretData ); + }, [ currentCaretData, onScrollToTextInput ] ); + + // When the orientation changes, the ScrollView measurements + // need to be re-calculated. + useEffect( () => { + // Only re-caculate them if there's an existing value + // as it should be set when the ScrollView content changes. + if ( scrollViewMeasurements.current ) { + measureScrollView(); } - }, [ - currentCaretData, - isKeyboardVisible, - keyboardOffset, - onScrollToTextInput, - ] ); + }, [ isLandscape, measureScrollView ] ); const scrollHandler = useAnimatedScrollHandler( { onScroll: ( event ) => { @@ -91,9 +106,24 @@ export const KeyboardAwareFlatList = ( { }, } ); + const measureScrollView = useCallback( () => { + if ( scrollViewRef.current ) { + const scrollRef = scrollViewRef.current.getNativeScrollRef(); + + scrollRef.measureInWindow( ( _x, y, width, height ) => { + scrollViewMeasurements.current = { y, width, height }; + } ); + } + }, [] ); + const onContentSizeChange = useCallback( () => { onScrollToTextInput( currentCaretData ); - }, [ onScrollToTextInput, currentCaretData ] ); + + // Sets the first values when the content size changes. + if ( ! scrollViewMeasurements.current ) { + measureScrollView(); + } + }, [ measureScrollView, onScrollToTextInput, currentCaretData ] ); const getRef = useCallback( ( ref ) => { diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js index f9b6202ad05a83..98c33cd4750fbb 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js @@ -9,29 +9,24 @@ import { renderHook } from '@testing-library/react-native'; */ import useScrollToTextInput from '../use-scroll-to-text-input'; -const mockUseWindowDimensions = jest.fn(); -jest.mock( 'react-native/Libraries/Utilities/useWindowDimensions', () => ( { - default: mockUseWindowDimensions, -} ) ); - describe( 'useScrollToTextInput', () => { - it( 'scrolls up to the TextInput offset when the caret is at the top of the screen', () => { + it( 'scrolls up to the current TextInput offset', () => { // Arrange const currentCaretData = { caretHeight: 10 }; const extraScrollHeight = 50; const keyboardOffset = 100; const scrollEnabled = true; const scrollViewRef = { current: { scrollTo: jest.fn() } }; + const scrollViewMeasurements = { current: { height: 600 } }; const scrollViewYOffset = { value: 150 }; const textInputOffset = 50; - const windowHeight = 600; - mockUseWindowDimensions.mockReturnValue( { height: windowHeight } ); const { result } = renderHook( () => useScrollToTextInput( extraScrollHeight, keyboardOffset, scrollEnabled, + scrollViewMeasurements, scrollViewRef, scrollViewYOffset ) @@ -42,28 +37,28 @@ describe( 'useScrollToTextInput', () => { // Assert expect( scrollViewRef.current.scrollTo ).toHaveBeenCalledWith( { - y: scrollViewYOffset.value - currentCaretData.caretHeight * 2, + y: textInputOffset, animated: true, } ); } ); - it( 'scrolls down to the TextInput offset when the caret is at the bottom of the screen', () => { + it( 'scrolls down to the current TextInput offset', () => { // Arrange const currentCaretData = { caretHeight: 10 }; const extraScrollHeight = 50; const keyboardOffset = 100; const scrollEnabled = true; const scrollViewRef = { current: { scrollTo: jest.fn() } }; - const scrollViewYOffset = { value: 0 }; - const textInputOffset = 550; - const windowHeight = 600; - mockUseWindowDimensions.mockReturnValue( { height: windowHeight } ); + const scrollViewMeasurements = { current: { height: 600 } }; + const scrollViewYOffset = { value: 250 }; + const textInputOffset = 750; const { result } = renderHook( () => useScrollToTextInput( extraScrollHeight, keyboardOffset, scrollEnabled, + scrollViewMeasurements, scrollViewRef, scrollViewYOffset ) @@ -74,10 +69,11 @@ describe( 'useScrollToTextInput', () => { // Assert const expectedYOffset = - textInputOffset + - extraScrollHeight - - ( windowHeight - ( keyboardOffset + extraScrollHeight ) ) + - currentCaretData.caretHeight * 2; + textInputOffset - + ( scrollViewMeasurements.current.height - + ( keyboardOffset + + extraScrollHeight + + currentCaretData.caretHeight ) ); expect( scrollViewRef.current.scrollTo ).toHaveBeenCalledWith( { y: expectedYOffset, animated: true, @@ -91,16 +87,16 @@ describe( 'useScrollToTextInput', () => { const keyboardOffset = 100; const scrollEnabled = true; const scrollViewRef = { current: null }; + const scrollViewMeasurements = { current: { height: 600 } }; const scrollViewYOffset = { value: 0 }; const textInputOffset = 50; - const windowHeight = 600; - mockUseWindowDimensions.mockReturnValue( { height: windowHeight } ); const { result } = renderHook( () => useScrollToTextInput( extraScrollHeight, keyboardOffset, scrollEnabled, + scrollViewMeasurements, scrollViewRef, scrollViewYOffset ) @@ -120,16 +116,16 @@ describe( 'useScrollToTextInput', () => { const keyboardOffset = 100; const scrollEnabled = false; const scrollViewRef = { current: { scrollTo: jest.fn() } }; + const scrollViewMeasurements = { current: { height: 600 } }; const scrollViewYOffset = { value: 0 }; const textInputOffset = 50; - const windowHeight = 600; - mockUseWindowDimensions.mockReturnValue( { height: windowHeight } ); const { result } = renderHook( () => useScrollToTextInput( extraScrollHeight, keyboardOffset, scrollEnabled, + scrollViewMeasurements, scrollViewRef, scrollViewYOffset ) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js index bd69642c43cbeb..13610bb568c887 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js @@ -1,9 +1,3 @@ -/** - * External dependencies - */ - -import { useWindowDimensions } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; /** * WordPress dependencies */ @@ -18,81 +12,62 @@ const DEFAULT_FONT_SIZE = 16; * depending on where the caret is placed taking into * account the Keyboard and the Header. * - * @param {number} extraScrollHeight Extra space to not overlap the content. - * @param {number} keyboardOffset Keyboard space offset. - * @param {boolean} scrollEnabled Whether the scroll is enabled or not. - * @param {RefObject} scrollViewRef ScrollView reference. - * @param {SharedValue} scrollViewYOffset Current offset position of the ScrollView. + * @param {number} extraScrollHeight Extra space to not overlap the content. + * @param {number} keyboardOffset Keyboard space offset. + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {RefObject} scrollViewMeasurements ScrollView Layout measurements. + * @param {RefObject} scrollViewRef ScrollView reference. + * @param {SharedValue} scrollViewYOffset Current offset position of the ScrollView. * @return {Function[]} Function to scroll to the current TextInput's offset. */ export default function useScrollToTextInput( extraScrollHeight, keyboardOffset, scrollEnabled, + scrollViewMeasurements, scrollViewRef, scrollViewYOffset ) { - const { height: windowHeight } = useWindowDimensions(); - const { top, bottom } = useSafeAreaInsets(); - const availableScreenOffset = Math.round( - windowHeight - ( top + bottom ) - ( keyboardOffset + extraScrollHeight ) - ); - - const shouldScrollUp = useCallback( - ( textInputOffset, caretHeight ) => { - const offset = textInputOffset - caretHeight; - return offset < scrollViewYOffset.value; - }, - [ scrollViewYOffset ] - ); - - const shouldScrollDown = useCallback( - ( textInputOffset, extraPadding ) => { - const offset = - scrollViewYOffset.value + availableScreenOffset - extraPadding; - return textInputOffset > offset; - }, - [ availableScreenOffset, scrollViewYOffset ] - ); - const scrollToTextInputOffset = useCallback( ( caret, textInputOffset ) => { const { caretHeight = DEFAULT_FONT_SIZE } = caret ?? {}; - const extraPadding = caretHeight * 2; - if ( ! scrollViewRef.current || ! scrollEnabled ) { + if ( + ! scrollViewRef.current || + ! scrollEnabled || + ! scrollViewMeasurements.current + ) { return; } + const availableScreenSpace = + scrollViewMeasurements.current.height - + ( keyboardOffset + extraScrollHeight + caretHeight ); + const maxOffset = scrollViewYOffset.value + availableScreenSpace; - if ( shouldScrollUp( textInputOffset, caretHeight ) ) { - const scrollUpOffset = scrollViewYOffset.value - extraPadding; + // Scroll up. + if ( textInputOffset < scrollViewYOffset.value ) { scrollViewRef.current.scrollTo( { - y: scrollUpOffset, + y: textInputOffset, animated: true, } ); return; } - if ( shouldScrollDown( textInputOffset, extraPadding ) ) { - const scrollDownOffset = - textInputOffset + - extraScrollHeight - - availableScreenOffset + - extraPadding; + // Scroll down. + if ( textInputOffset > maxOffset ) { scrollViewRef.current.scrollTo( { - y: scrollDownOffset, + y: textInputOffset - availableScreenSpace, animated: true, } ); } }, [ - availableScreenOffset, extraScrollHeight, + keyboardOffset, scrollEnabled, + scrollViewMeasurements, scrollViewRef, - scrollViewYOffset, - shouldScrollDown, - shouldScrollUp, + scrollViewYOffset.value, ] ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js index e4418baaa70c2c..81e10a5571d930 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js @@ -26,8 +26,11 @@ export default function useTextInputOffset( scrollEnabled, scrollViewRef ) { scrollViewRef.current, ( _x, y, _width, height ) => { const caretYOffset = - // For cases where the caretY value is -1 we use the y + height value. - caretY >= 0 && caretY < height + // For cases where the caretY value is -1 or null + // we use the y + height value. + caretY !== null && + caretY >= 0 && + caretY < height ? y + caretY : y + height; resolve( Math.round( Math.abs( caretYOffset ) ) ); From a94a57ae8c4de6f17f9525e0f48db34eaf4276a0 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 24 Mar 2023 10:55:48 +0100 Subject: [PATCH 20/48] Mobile - Keyboard Aware Flatlist - Add missing styles for inner blocks --- .../block-editor/src/components/block-list/index.native.js | 6 ++++++ .../src/mobile/keyboard-aware-flat-list/index.ios.js | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index da56f789da5cc1..b70a4d4658b6e1 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -230,6 +230,11 @@ export class BlockList extends Component { blockToolbar.height + ( isFloatingToolbarVisible ? floatingToolbar.height : 0 ); + const scrollViewStyle = [ + { flex: isRootList ? 1 : 0 }, + ! isRootList && styles.overflowVisible, + ]; + return ( { @@ -137,6 +138,8 @@ export const KeyboardAwareFlatList = ( { // extra padding at the bottom. const contentInset = { bottom: keyboardOffset }; + const style = [ { flex: 1 }, scrollViewStyle ]; + return ( From 01152ad868fb3da370041096e3e339c74a995815 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 24 Mar 2023 10:56:36 +0100 Subject: [PATCH 21/48] Mobile - Keyboard Aware Flatlist - Fix issue when the title is focused and it shouldn't scroll down --- .../use-scroll-to-text-input.native.js | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js index 13610bb568c887..016b79f57597fc 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + /** * WordPress dependencies */ @@ -28,6 +33,9 @@ export default function useScrollToTextInput( scrollViewRef, scrollViewYOffset ) { + const { top, bottom } = useSafeAreaInsets(); + const insets = top + bottom; + const scrollToTextInputOffset = useCallback( ( caret, textInputOffset ) => { const { caretHeight = DEFAULT_FONT_SIZE } = caret ?? {}; @@ -39,13 +47,13 @@ export default function useScrollToTextInput( ) { return; } - const availableScreenSpace = - scrollViewMeasurements.current.height - - ( keyboardOffset + extraScrollHeight + caretHeight ); - const maxOffset = scrollViewYOffset.value + availableScreenSpace; + const currentScrollViewYOffset = Math.max( + 0, + scrollViewYOffset.value + ); // Scroll up. - if ( textInputOffset < scrollViewYOffset.value ) { + if ( textInputOffset < currentScrollViewYOffset ) { scrollViewRef.current.scrollTo( { y: textInputOffset, animated: true, @@ -53,8 +61,21 @@ export default function useScrollToTextInput( return; } + const availableScreenSpace = Math.abs( + Math.floor( + scrollViewMeasurements.current.height - + ( keyboardOffset + extraScrollHeight + caretHeight ) + ) + ); + const maxOffset = Math.floor( + currentScrollViewYOffset + availableScreenSpace + ); + + const isAtTheTop = + textInputOffset < scrollViewMeasurements.current.y + insets; + // Scroll down. - if ( textInputOffset > maxOffset ) { + if ( textInputOffset > maxOffset && ! isAtTheTop ) { scrollViewRef.current.scrollTo( { y: textInputOffset - availableScreenSpace, animated: true, @@ -63,11 +84,12 @@ export default function useScrollToTextInput( }, [ extraScrollHeight, + insets, keyboardOffset, scrollEnabled, scrollViewMeasurements, scrollViewRef, - scrollViewYOffset.value, + scrollViewYOffset, ] ); From d336b42989e054b8132f3c691b392afadc383879 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 24 Mar 2023 18:18:06 +0100 Subject: [PATCH 22/48] Mobile - Keyboard Aware Flatlist - Don't take into account null values --- .../use-text-input-offset.native.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js index 81e10a5571d930..30ebab0495ace5 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js @@ -26,11 +26,9 @@ export default function useTextInputOffset( scrollEnabled, scrollViewRef ) { scrollViewRef.current, ( _x, y, _width, height ) => { const caretYOffset = - // For cases where the caretY value is -1 or null + // For cases where the caretY value is -1 // we use the y + height value. - caretY !== null && - caretY >= 0 && - caretY < height + caretY >= 0 && caretY < height ? y + caretY : y + height; resolve( Math.round( Math.abs( caretYOffset ) ) ); From 40e37747ebc1001b296efbb7d89a9f77d5ae2486 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 24 Mar 2023 18:18:40 +0100 Subject: [PATCH 23/48] Mobile - AztecView - iOS: Pass the caret data when the TextInput is focused --- .../ios/RNTAztecView/RCTAztecView.swift | 3 ++- packages/react-native-aztec/src/AztecView.js | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift index c66bbb9c9b03e9..2cfbe3c0b00089 100644 --- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift +++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift @@ -793,7 +793,8 @@ extension RCTAztecView: UITextViewDelegate { override func becomeFirstResponder() -> Bool { if !isFirstResponder && canBecomeFirstResponder { - onFocus?([:]) + let caretData = packCaretDataForRN() + onFocus?(caretData) } return super.becomeFirstResponder() } diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index 7505dbf71fd2c6..fa8e4327b35564 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -197,8 +197,12 @@ class AztecView extends Component { onSelectionChange( selectionStart, selectionEnd, text, event ); } + this.updateCaretData( event ); + } + + updateCaretData( event ) { if ( - this.selectionEndCaretY !== event.nativeEvent.selectionEndCaretY + this.selectionEndCaretY !== event?.nativeEvent?.selectionEndCaretY ) { const caretY = event.nativeEvent.selectionEndCaretY; AztecInputState.setCurrentCaretData( { @@ -235,6 +239,8 @@ class AztecView extends Component { // combination generate an infinite loop as described in https://github.com/wordpress-mobile/gutenberg-mobile/issues/302 // For iOS, this is necessary to let the system know when Aztec was focused programatically. if ( Platform.OS === 'ios' ) { + this.updateCaretData( event ); + this._onPress( event ); } } From be90fad6bbf91bb3e26efa431c84e1bf15de8a26 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 28 Mar 2023 17:21:50 +0200 Subject: [PATCH 24/48] Mobile - Keyboard Aware FlatList - Remove unused shouldPreventAutomaticScroll prop --- .../components/src/mobile/keyboard-aware-flat-list/index.ios.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 0b337f4212b1f7..3a69780070fdfc 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -30,7 +30,6 @@ export const KeyboardAwareFlatList = ( { onScroll, scrollEnabled, scrollViewStyle, - shouldPreventAutomaticScroll, ...props } ) => { const scrollViewRef = useRef(); From 72277f1e7a66ba2c93ebdea67843e300444de23f Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 28 Mar 2023 18:01:06 +0200 Subject: [PATCH 25/48] Mobile - Keyboard Aware FlatList - useKeyboardOffset - Add case where it should remove listeners if the scrollEnabled prop changes from true to false --- .../test/use-keyboard-offset.native.js | 63 +++++++++++++++++++ .../use-keyboard-offset.native.js | 5 ++ 2 files changed, 68 insertions(+) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js index 8d68eb6196adea..1e6445d3dd6b47 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js @@ -66,4 +66,67 @@ describe( 'useKeyboardOffset', () => { expect( isKeyboardVisible ).toBe( false ); expect( keyboardOffset ).toBe( 0 ); } ); + + it( 'removes all keyboard listeners when scrollEnabled changes to false', () => { + // Arrange + const { result, rerender } = renderHook( + ( { scrollEnabled } ) => useKeyboardOffset( scrollEnabled ), + { + initialProps: { scrollEnabled: true }, + } + ); + const [ isKeyboardVisible, keyboardOffset ] = result.current; + + // Act + rerender( { scrollEnabled: false } ); + + // Assert + expect( isKeyboardVisible ).toBe( false ); + expect( keyboardOffset ).toBe( 0 ); + expect( + RCTDeviceEventEmitter.listenerCount( 'keyboardWillShow' ) + ).toBe( 0 ); + expect( RCTDeviceEventEmitter.listenerCount( 'keyboardDidShow' ) ).toBe( + 0 + ); + expect( + RCTDeviceEventEmitter.listenerCount( 'keyboardWillHide' ) + ).toBe( 0 ); + } ); + + it( 'adds all keyboard listeners when scrollEnabled changes to true', () => { + // Arrange + const { result, rerender } = renderHook( + ( { scrollEnabled } ) => useKeyboardOffset( scrollEnabled ), + { + initialProps: { scrollEnabled: false }, + } + ); + // Act + act( () => { + rerender( { scrollEnabled: true } ); + } ); + + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardWillShow' ); + RCTDeviceEventEmitter.emit( 'keyboardDidShow', { + endCoordinates: { height: 250 }, + } ); + } ); + + const [ isKeyboardVisible, keyboardOffset ] = result.current; + + // Assert + expect( isKeyboardVisible ).toBe( true ); + expect( keyboardOffset ).toBe( 250 ); + expect( + RCTDeviceEventEmitter.listenerCount( 'keyboardWillShow' ) + ).toBe( 1 ); + expect( RCTDeviceEventEmitter.listenerCount( 'keyboardDidShow' ) ).toBe( + 1 + ); + expect( + RCTDeviceEventEmitter.listenerCount( 'keyboardWillHide' ) + ).toBe( 1 ); + } ); } ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js index aa11cdd18c7412..4bb274ea4d4d3a 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js @@ -48,7 +48,12 @@ export default function useKeyboardOffset( scrollEnabled ) { } setIsKeyboardVisible( false ); } ); + } else { + willShowSubscription?.remove(); + showSubscription?.remove(); + hideSubscription?.remove(); } + return () => { willShowSubscription?.remove(); showSubscription?.remove(); From 89ba9330bed3073f132d50d4927dc17a54bcffda Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 28 Mar 2023 18:05:18 +0200 Subject: [PATCH 26/48] Mobile - Keyboard Aware FlatList - useTextInputOffset: Prevent measuring the TextInput if the caretY is null since we are no longer taking that as a valid value --- .../use-text-input-offset.native.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js index 30ebab0495ace5..9c6d3207afa9bb 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js @@ -21,7 +21,12 @@ export default function useTextInputOffset( scrollEnabled, scrollViewRef ) { RCTAztecView.InputState.getCurrentFocusedElement(); return new Promise( ( resolve ) => { - if ( scrollViewRef.current && textInput && scrollEnabled ) { + if ( + scrollViewRef.current && + textInput && + scrollEnabled && + caretY !== null + ) { textInput.measureLayout( scrollViewRef.current, ( _x, y, _width, height ) => { From fe189fef2e68f76f2e089b620140c60834b740e6 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 28 Mar 2023 18:14:47 +0200 Subject: [PATCH 27/48] Mobile - Keyboard Aware FlatList - useScrollToTextInput: Add documentation for scrollToTextInputOffset --- .../use-scroll-to-text-input.native.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js index 016b79f57597fc..abf34d58148f2b 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js @@ -36,6 +36,14 @@ export default function useScrollToTextInput( const { top, bottom } = useSafeAreaInsets(); const insets = top + bottom; + /** + * Function to scroll to the current TextInput's offset. + * + * @param {Object} caret - The caret position data of the currently focused TextInput. + * @param {number} caret.caretHeight - The height of the caret. + * @param {number} textInputOffset - The offset calculated with the caret's Y coordinate + the + * - TextInput's Y coord or height value. + */ const scrollToTextInputOffset = useCallback( ( caret, textInputOffset ) => { const { caretHeight = DEFAULT_FONT_SIZE } = caret ?? {}; From fd42475f80cf7e270d05dfba95d22e6f490f874e Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 28 Mar 2023 18:36:23 +0200 Subject: [PATCH 28/48] Mobile - Keyboard Aware FlatList - useScrollToTextInput: Update "does not scroll when the ScrollView ref is not available" test to check scrollTo wasn't called --- .../test/use-scroll-to-text-input.native.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js index 98c33cd4750fbb..f580138bd53e7f 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js @@ -91,6 +91,12 @@ describe( 'useScrollToTextInput', () => { const scrollViewYOffset = { value: 0 }; const textInputOffset = 50; + // Mock scrollTo method + const scrollTo = jest.fn(); + if ( scrollViewRef.current ) { + scrollViewRef.current.scrollTo = scrollTo; + } + const { result } = renderHook( () => useScrollToTextInput( extraScrollHeight, @@ -107,6 +113,7 @@ describe( 'useScrollToTextInput', () => { // Assert expect( scrollViewRef.current ).toBeNull(); + expect( scrollTo ).not.toHaveBeenCalled(); } ); it( 'does not scroll when the scroll is not enabled', () => { From 2e8853cf3db666e2ffcf1014cd26dc87ff1861fa Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 28 Mar 2023 18:56:52 +0200 Subject: [PATCH 29/48] Mobile - AztecInputState - Fix spacing in comment --- packages/react-native-aztec/src/AztecInputState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index e47d5576224332..132cd36010fb18 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -72,7 +72,7 @@ export const removeCaretChangeListener = ( listener ) => { }; /** - * Notifies listeners about caret changes in focused Aztec view. + * Notifies listeners about caret changes in focused Aztec view. */ const notifyCaretChangeListeners = () => { caretChangeListeners.forEach( ( listener ) => { From eedca15d1d3fcd919c0616a4444a23d2b37c7b56 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 28 Mar 2023 20:24:29 +0200 Subject: [PATCH 30/48] Mobile - E2E Tests - Paragraph and Block Insertion: Remove adding isAndroid conditions --- .../gutenberg-editor-block-insertion-@canary.test.js | 8 ++------ .../__device-tests__/gutenberg-editor-paragraph.test.js | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js index 311196345f6b13..b413f3b6a42cea 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-block-insertion-@canary.test.js @@ -43,9 +43,7 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } + await paragraphBlockElement.click(); await editorPage.removeBlock(); } } ); @@ -87,9 +85,7 @@ describe( 'Gutenberg Editor tests for Block insertion', () => { paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } + await paragraphBlockElement.click(); await editorPage.removeBlock(); } } ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js index 1d89c322173c05..aa398d1f24e265 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js @@ -88,9 +88,7 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { for ( let i = 3; i > 0; i-- ) { const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } + await paragraphBlockElement.click(); await editorPage.removeBlock(); } } ); From e7d9b3818ff30097dc8fb050c18da5a0ffe09f05 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 29 Mar 2023 11:45:43 +0200 Subject: [PATCH 31/48] Revert "Mobile - Keyboard Aware FlatList - useScrollToTextInput: Update "does not scroll when the ScrollView ref is not available" test to check scrollTo wasn't called" This reverts commit fd42475f80cf7e270d05dfba95d22e6f490f874e. --- .../test/use-scroll-to-text-input.native.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js index f580138bd53e7f..98c33cd4750fbb 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll-to-text-input.native.js @@ -91,12 +91,6 @@ describe( 'useScrollToTextInput', () => { const scrollViewYOffset = { value: 0 }; const textInputOffset = 50; - // Mock scrollTo method - const scrollTo = jest.fn(); - if ( scrollViewRef.current ) { - scrollViewRef.current.scrollTo = scrollTo; - } - const { result } = renderHook( () => useScrollToTextInput( extraScrollHeight, @@ -113,7 +107,6 @@ describe( 'useScrollToTextInput', () => { // Assert expect( scrollViewRef.current ).toBeNull(); - expect( scrollTo ).not.toHaveBeenCalled(); } ); it( 'does not scroll when the scroll is not enabled', () => { From a8341efbcffea60b3d7a6bad6c0abf962c04d0d2 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 29 Mar 2023 15:54:23 +0200 Subject: [PATCH 32/48] Mobile - KeyboardAwareFlatList - Reset scrollViewMeasurements to null everythime the dependecies change --- .../components/src/mobile/keyboard-aware-flat-list/index.ios.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 3a69780070fdfc..63c59e6245ea8d 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -91,6 +91,7 @@ export const KeyboardAwareFlatList = ( { // When the orientation changes, the ScrollView measurements // need to be re-calculated. useEffect( () => { + scrollViewMeasurements.current = null; // Only re-caculate them if there's an existing value // as it should be set when the ScrollView content changes. if ( scrollViewMeasurements.current ) { From b2ffb2d34bd5da74afaa137d234fedfa7cd00c44 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 29 Mar 2023 15:55:22 +0200 Subject: [PATCH 33/48] Mobile - useKeyboardOffset - Remove if condition if there's an AztecView currently focused, it is not needed anymore --- .../use-keyboard-offset.native.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js index 4bb274ea4d4d3a..cf9d21d2883f0b 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js @@ -7,7 +7,6 @@ import { Keyboard } from 'react-native'; /** * WordPress dependencies */ -import RCTAztecView from '@wordpress/react-native-aztec'; import { useEffect, useState } from '@wordpress/element'; /** @@ -40,12 +39,7 @@ export default function useKeyboardOffset( scrollEnabled ) { } ); hideSubscription = Keyboard.addListener( 'keyboardWillHide', () => { - // Changing focus between TextInputs triggers this listener as the - // Keyboard gets dimissed and then shows up again, so it's needed to - // avoid setting the keyboard offset to 0 unless there's no focused input. - if ( ! RCTAztecView.InputState.isFocused() ) { - setKeyboardOffset( 0 ); - } + setKeyboardOffset( 0 ); setIsKeyboardVisible( false ); } ); } else { From 45fff3876fda1be3bdba183a74840264ab5f965f Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 29 Mar 2023 15:56:32 +0200 Subject: [PATCH 34/48] Mobile - AztecView - Pass caret data when the content size of the TextInput changes e.g the orientation changes. Also update the caret data if the AztecView is focused --- .../react-native-aztec/ios/RNTAztecView/RCTAztecView.swift | 5 ++++- packages/react-native-aztec/src/AztecView.js | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift index 2cfbe3c0b00089..c6e928b3964404 100644 --- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift +++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift @@ -234,7 +234,10 @@ class RCTAztecView: Aztec.TextView { previousContentSize = newSize let body = packForRN(newSize, withName: "contentSize") - onContentSizeChange(body) + let caretData = packCaretDataForRN() + var result = body + result.merge(caretData) { (_, new) in new } + onContentSizeChange(result) } // MARK: - Paste handling diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index fa8e4327b35564..4d90d13974c8ec 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -79,6 +79,8 @@ class AztecView extends Component { } _onContentSizeChange( event ) { + this.updateCaretData( event ); + if ( ! this.props.onContentSizeChange ) { return; } @@ -202,6 +204,7 @@ class AztecView extends Component { updateCaretData( event ) { if ( + this.isFocused() && this.selectionEndCaretY !== event?.nativeEvent?.selectionEndCaretY ) { const caretY = event.nativeEvent.selectionEndCaretY; From 4374115229be41446d081fd921fcd6577da5e68c Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 31 Mar 2023 14:50:45 +0200 Subject: [PATCH 35/48] Mobile - AztecInputState - Don't notify caret change listeners if there's no caret data (avoid triggering them with null values) --- packages/react-native-aztec/src/AztecInputState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 132cd36010fb18..8a1737118d1d13 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -152,7 +152,7 @@ export const blurCurrentFocusedElement = () => { * @param {Object} caret Caret's data. */ export const setCurrentCaretData = ( caret ) => { - if ( isFocused() ) { + if ( isFocused() && caret ) { currentCaretData = caret; notifyCaretChangeListeners(); } From bf36349aaa13d09dcc174554599da9d60d752d66 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 31 Mar 2023 14:53:06 +0200 Subject: [PATCH 36/48] Mobile - useKeyboardOffset: Remove usage of keyboardWillShow and just rely on keyboardDidShow and keyboardDidHide, this will be useful when this logic is shared with Android. It also updates the hook to just store the current keyboard offset avoiding storing the keyboard visibility as well. --- .../test/use-keyboard-offset.native.js | 41 ++++++------------- .../use-keyboard-offset.native.js | 38 ++++++++--------- 2 files changed, 30 insertions(+), 49 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js index 1e6445d3dd6b47..b5e374fb799b26 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js @@ -12,18 +12,16 @@ import useKeyboardOffset from '../use-keyboard-offset'; describe( 'useKeyboardOffset', () => { beforeEach( () => { - Keyboard.removeAllListeners( 'keyboardWillShow' ); Keyboard.removeAllListeners( 'keyboardDidShow' ); - Keyboard.removeAllListeners( 'keyboardWillHide' ); + Keyboard.removeAllListeners( 'keyboardDidHide' ); } ); it( 'returns the initial state', () => { // Arrange const { result } = renderHook( () => useKeyboardOffset( true ) ); - const [ isKeyboardVisible, keyboardOffset ] = result.current; + const [ keyboardOffset ] = result.current; // Assert - expect( isKeyboardVisible ).toBe( false ); expect( keyboardOffset ).toBe( 0 ); } ); @@ -33,15 +31,13 @@ describe( 'useKeyboardOffset', () => { // Act act( () => { - RCTDeviceEventEmitter.emit( 'keyboardWillShow' ); RCTDeviceEventEmitter.emit( 'keyboardDidShow', { endCoordinates: { height: 250 }, } ); } ); // Assert - const [ isKeyboardVisible, keyboardOffset ] = result.current; - expect( isKeyboardVisible ).toBe( true ); + const [ keyboardOffset ] = result.current; expect( keyboardOffset ).toBe( 250 ); } ); @@ -51,19 +47,17 @@ describe( 'useKeyboardOffset', () => { // Act act( () => { - RCTDeviceEventEmitter.emit( 'keyboardWillShow' ); RCTDeviceEventEmitter.emit( 'keyboardDidShow', { endCoordinates: { height: 250 }, } ); } ); act( () => { - RCTDeviceEventEmitter.emit( 'keyboardWillHide' ); + RCTDeviceEventEmitter.emit( 'keyboardDidHide' ); } ); // Assert - const [ isKeyboardVisible, keyboardOffset ] = result.current; - expect( isKeyboardVisible ).toBe( false ); + const [ keyboardOffset ] = result.current; expect( keyboardOffset ).toBe( 0 ); } ); @@ -75,23 +69,19 @@ describe( 'useKeyboardOffset', () => { initialProps: { scrollEnabled: true }, } ); - const [ isKeyboardVisible, keyboardOffset ] = result.current; + const [ keyboardOffset ] = result.current; // Act rerender( { scrollEnabled: false } ); // Assert - expect( isKeyboardVisible ).toBe( false ); expect( keyboardOffset ).toBe( 0 ); - expect( - RCTDeviceEventEmitter.listenerCount( 'keyboardWillShow' ) - ).toBe( 0 ); + expect( RCTDeviceEventEmitter.listenerCount( 'keyboardDidHide' ) ).toBe( + 0 + ); expect( RCTDeviceEventEmitter.listenerCount( 'keyboardDidShow' ) ).toBe( 0 ); - expect( - RCTDeviceEventEmitter.listenerCount( 'keyboardWillHide' ) - ).toBe( 0 ); } ); it( 'adds all keyboard listeners when scrollEnabled changes to true', () => { @@ -108,25 +98,20 @@ describe( 'useKeyboardOffset', () => { } ); act( () => { - RCTDeviceEventEmitter.emit( 'keyboardWillShow' ); RCTDeviceEventEmitter.emit( 'keyboardDidShow', { endCoordinates: { height: 250 }, } ); } ); - const [ isKeyboardVisible, keyboardOffset ] = result.current; + const [ keyboardOffset ] = result.current; // Assert - expect( isKeyboardVisible ).toBe( true ); expect( keyboardOffset ).toBe( 250 ); - expect( - RCTDeviceEventEmitter.listenerCount( 'keyboardWillShow' ) - ).toBe( 1 ); expect( RCTDeviceEventEmitter.listenerCount( 'keyboardDidShow' ) ).toBe( 1 ); - expect( - RCTDeviceEventEmitter.listenerCount( 'keyboardWillHide' ) - ).toBe( 1 ); + expect( RCTDeviceEventEmitter.listenerCount( 'keyboardDidHide' ) ).toBe( + 1 + ); } ); } ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js index cf9d21d2883f0b..ceafaf2c76eeb4 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js @@ -7,52 +7,48 @@ import { Keyboard } from 'react-native'; /** * WordPress dependencies */ -import { useEffect, useState } from '@wordpress/element'; +import { useEffect, useCallback, useState } from '@wordpress/element'; /** * Hook that adds Keyboard listeners to get the offset space * when the keyboard is opened, taking into account focused AztecViews. * * @param {boolean} scrollEnabled Whether the scroll is enabled or not. - * @return {[boolean, number]} Keyboard visibility state and Keyboard offset. + * @return {[number]} Keyboard offset. */ export default function useKeyboardOffset( scrollEnabled ) { const [ keyboardOffset, setKeyboardOffset ] = useState( 0 ); - const [ isKeyboardVisible, setIsKeyboardVisible ] = useState( false ); + + const onKeyboardDidShow = useCallback( ( { endCoordinates } ) => { + setKeyboardOffset( endCoordinates.height ); + }, [] ); + + const onKeyboardDidHide = useCallback( () => { + setKeyboardOffset( 0 ); + }, [] ); useEffect( () => { - let willShowSubscription; let showSubscription; let hideSubscription; if ( scrollEnabled ) { - willShowSubscription = Keyboard.addListener( - 'keyboardWillShow', - () => { - setIsKeyboardVisible( true ); - } - ); showSubscription = Keyboard.addListener( 'keyboardDidShow', - ( { endCoordinates } ) => { - setKeyboardOffset( endCoordinates.height ); - } + onKeyboardDidShow + ); + hideSubscription = Keyboard.addListener( + 'keyboardDidHide', + onKeyboardDidHide ); - hideSubscription = Keyboard.addListener( 'keyboardWillHide', () => { - setKeyboardOffset( 0 ); - setIsKeyboardVisible( false ); - } ); } else { - willShowSubscription?.remove(); showSubscription?.remove(); hideSubscription?.remove(); } return () => { - willShowSubscription?.remove(); showSubscription?.remove(); hideSubscription?.remove(); }; - }, [ scrollEnabled ] ); - return [ isKeyboardVisible, keyboardOffset ]; + }, [ scrollEnabled, onKeyboardDidShow, onKeyboardDidHide ] ); + return [ keyboardOffset ]; } From fdda82f9e9234df8b2cafcb1532705f5128e5862 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 31 Mar 2023 14:54:57 +0200 Subject: [PATCH 37/48] Mobile - KeyboardAwareFlatList - Remove usage of isKeboardVisible since we just need to know if there's a keyboard offset set or not. It also removes measureScrollView from the useEffect that listens to device orientation changes --- .../keyboard-aware-flat-list/index.ios.js | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 63c59e6245ea8d..6d159969215c90 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -39,8 +39,7 @@ export const KeyboardAwareFlatList = ( { const { height: windowHeight, width: windowWidth } = useWindowDimensions(); const isLandscape = windowWidth >= windowHeight; - const [ isKeyboardVisible, keyboardOffset ] = - useKeyboardOffset( scrollEnabled ); + const [ keyboardOffset ] = useKeyboardOffset( scrollEnabled ); const [ currentCaretData ] = useTextInputCaretPosition( scrollEnabled ); @@ -62,23 +61,13 @@ export const KeyboardAwareFlatList = ( { useCallback( async ( caret ) => { const textInputOffset = await getTextInputOffset( caret ); - const isKeyboardVisibleWithOffset = - isKeyboardVisible && keyboardOffset !== 0; const hasTextInputOffset = textInputOffset !== null; - if ( - ( isKeyboardVisibleWithOffset && hasTextInputOffset ) || - ( ! isKeyboardVisible && hasTextInputOffset ) - ) { + if ( hasTextInputOffset ) { scrollToTextInputOffset( caret, textInputOffset ); } }, - [ - getTextInputOffset, - isKeyboardVisible, - keyboardOffset, - scrollToTextInputOffset, - ] + [ getTextInputOffset, scrollToTextInputOffset ] ), 200, { leading: false } @@ -92,12 +81,7 @@ export const KeyboardAwareFlatList = ( { // need to be re-calculated. useEffect( () => { scrollViewMeasurements.current = null; - // Only re-caculate them if there's an existing value - // as it should be set when the ScrollView content changes. - if ( scrollViewMeasurements.current ) { - measureScrollView(); - } - }, [ isLandscape, measureScrollView ] ); + }, [ isLandscape ] ); const scrollHandler = useAnimatedScrollHandler( { onScroll: ( event ) => { From 8c31c522d62d8a67a3118fd8fcea7e8006e20b93 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 3 Apr 2023 17:12:52 +0200 Subject: [PATCH 38/48] Mobile - BlockList - Restore usage of shouldFlatListPreventAutomaticScroll --- .../src/components/block-list/index.native.js | 9 +++++++++ .../keyboard-aware-flat-list/index.ios.js | 20 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index b70a4d4658b6e1..02c79360b567aa 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -71,6 +71,8 @@ export class BlockList extends Component { this.renderBlockListFooter = this.renderBlockListFooter.bind( this ); this.scrollViewInnerRef = this.scrollViewInnerRef.bind( this ); this.addBlockToEndOfPost = this.addBlockToEndOfPost.bind( this ); + this.shouldFlatListPreventAutomaticScroll = + this.shouldFlatListPreventAutomaticScroll.bind( this ); this.shouldShowInnerBlockAppender = this.shouldShowInnerBlockAppender.bind( this ); this.renderEmptyList = this.renderEmptyList.bind( this ); @@ -93,6 +95,10 @@ export class BlockList extends Component { this.scrollViewRef = ref; } + shouldFlatListPreventAutomaticScroll() { + return this.props.isBlockInsertionPointVisible; + } + shouldShowInnerBlockAppender() { const { blockClientIds, renderAppender } = this.props; return renderAppender && blockClientIds.length > 0; @@ -271,6 +277,9 @@ export class BlockList extends Component { keyExtractor={ identity } renderItem={ this.renderItem } CellRendererComponent={ this.getCellRendererComponent } + shouldPreventAutomaticScroll={ + this.shouldFlatListPreventAutomaticScroll + } title={ title } ListHeaderComponent={ header } ListEmptyComponent={ ! isReadOnly && this.renderEmptyList } diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 6d159969215c90..7cfedf139dd370 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -24,12 +24,27 @@ import useTextInputCaretPosition from './use-text-input-caret-position'; const AnimatedScrollView = Animated.createAnimatedComponent( ScrollView ); +/** + * React component that provides a FlatList that is aware of the keyboard state and can scroll + * to the currently focused TextInput. + * + * @param {Object} props - Component props. + * @param {number} props.extraScrollHeight - Extra scroll height for the content. + * @param {Function} props.innerRef - Function to pass the ScrollView ref to the parent component. + * @param {Function} props.onScroll - Function to be called when the list is scrolled. + * @param {boolean} props.scrollEnabled - Whether the list can be scrolled. + * @param {Object} props.scrollViewStyle - Additional style for the ScrollView component. + * @param {boolean} props.shouldPreventAutomaticScroll - Whether to prevent scrolling when there's a Keyboard offset set. + * @param {Object} props... - Other props to pass to the FlatList component. + * @return {WPComponent} KeyboardAwareFlatList component. + */ export const KeyboardAwareFlatList = ( { extraScrollHeight, innerRef, onScroll, scrollEnabled, scrollViewStyle, + shouldPreventAutomaticScroll, ...props } ) => { const scrollViewRef = useRef(); @@ -39,7 +54,10 @@ export const KeyboardAwareFlatList = ( { const { height: windowHeight, width: windowWidth } = useWindowDimensions(); const isLandscape = windowWidth >= windowHeight; - const [ keyboardOffset ] = useKeyboardOffset( scrollEnabled ); + const [ keyboardOffset ] = useKeyboardOffset( + scrollEnabled, + shouldPreventAutomaticScroll + ); const [ currentCaretData ] = useTextInputCaretPosition( scrollEnabled ); From 85bd85a8f91838a811cf366d2ba36bae7747bc6c Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 3 Apr 2023 17:13:47 +0200 Subject: [PATCH 39/48] Mobile -useKeyboardOffset: Update hook to use a setTiemout to remove the keyboard offset, it also updates the unit test --- .../test/use-keyboard-offset.native.js | 109 +++++++++++++++++- .../use-keyboard-offset.native.js | 31 +++-- 2 files changed, 132 insertions(+), 8 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js index b5e374fb799b26..f3bca4583260da 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js @@ -10,6 +10,8 @@ import RCTDeviceEventEmitter from 'react-native/Libraries/EventEmitter/RCTDevice */ import useKeyboardOffset from '../use-keyboard-offset'; +jest.useFakeTimers(); + describe( 'useKeyboardOffset', () => { beforeEach( () => { Keyboard.removeAllListeners( 'keyboardDidShow' ); @@ -43,7 +45,10 @@ describe( 'useKeyboardOffset', () => { it( 'updates keyboard visibility and offset when the keyboard is hidden', () => { // Arrange - const { result } = renderHook( () => useKeyboardOffset( true ) ); + const shouldPreventAutomaticScroll = jest.fn().mockReturnValue( false ); + const { result } = renderHook( () => + useKeyboardOffset( true, shouldPreventAutomaticScroll ) + ); // Act act( () => { @@ -54,6 +59,7 @@ describe( 'useKeyboardOffset', () => { act( () => { RCTDeviceEventEmitter.emit( 'keyboardDidHide' ); + jest.runAllTimers(); } ); // Assert @@ -114,4 +120,105 @@ describe( 'useKeyboardOffset', () => { 1 ); } ); + + it( 'sets keyboard offset to 0 when keyboard is hidden and shouldPreventAutomaticScroll is false', () => { + // Arrange + const shouldPreventAutomaticScroll = jest.fn().mockReturnValue( false ); + const { result } = renderHook( () => + useKeyboardOffset( true, shouldPreventAutomaticScroll ) + ); + + // Act + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardDidShow', { + endCoordinates: { height: 250 }, + } ); + } ); + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardDidHide' ); + jest.runAllTimers(); + } ); + + // Assert + expect( result.current[ 0 ] ).toBe( 0 ); + } ); + + it( 'does not set keyboard offset to 0 when keyboard is hidden and shouldPreventAutomaticScroll is true', () => { + // Arrange + const shouldPreventAutomaticScroll = jest.fn().mockReturnValue( true ); + const { result } = renderHook( () => + useKeyboardOffset( true, shouldPreventAutomaticScroll ) + ); + + // Act + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardDidShow', { + endCoordinates: { height: 250 }, + } ); + } ); + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardDidHide' ); + jest.runAllTimers(); + } ); + + // Assert + expect( result.current[ 0 ] ).toBe( 250 ); + } ); + + it( 'handles updates to shouldPreventAutomaticScroll', () => { + // Arrange + const preventScrollTrue = jest.fn( () => true ); + const preventScrollFalse = jest.fn( () => false ); + + // Act + const { result, rerender } = renderHook( + ( { shouldPreventAutomaticScroll } ) => + useKeyboardOffset( true, shouldPreventAutomaticScroll ), + { + initialProps: { + shouldPreventAutomaticScroll: preventScrollFalse, + }, + } + ); + + // Assert + expect( result.current[ 0 ] ).toBe( 0 ); + + // Act + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardDidShow', { + endCoordinates: { height: 250 }, + } ); + } ); + + // Assert + expect( result.current[ 0 ] ).toBe( 250 ); + + // Act + act( () => { + rerender( { shouldPreventAutomaticScroll: preventScrollTrue } ); + } ); + + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardDidHide' ); + jest.runAllTimers(); + } ); + + // Assert + expect( result.current[ 0 ] ).toBe( 250 ); + + // Act + act( () => { + rerender( { shouldPreventAutomaticScroll: preventScrollFalse } ); + } ); + + act( () => { + RCTDeviceEventEmitter.emit( 'keyboardDidShow', { + endCoordinates: { height: 250 }, + } ); + } ); + + // Assert + expect( result.current[ 0 ] ).toBe( 250 ); + } ); } ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js index ceafaf2c76eeb4..f3a5c386d59048 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js @@ -7,26 +7,42 @@ import { Keyboard } from 'react-native'; /** * WordPress dependencies */ -import { useEffect, useCallback, useState } from '@wordpress/element'; +import { useEffect, useCallback, useState, useRef } from '@wordpress/element'; /** * Hook that adds Keyboard listeners to get the offset space * when the keyboard is opened, taking into account focused AztecViews. * - * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {boolean} scrollEnabled Whether the scroll is enabled or not. + * @param {Function} shouldPreventAutomaticScroll Whether to prevent scrolling when there's a Keyboard offset set. * @return {[number]} Keyboard offset. */ -export default function useKeyboardOffset( scrollEnabled ) { +export default function useKeyboardOffset( + scrollEnabled, + shouldPreventAutomaticScroll +) { const [ keyboardOffset, setKeyboardOffset ] = useState( 0 ); + const timeoutRef = useRef(); + + const onKeyboardDidHide = useCallback( () => { + if ( shouldPreventAutomaticScroll() ) { + clearTimeout( timeoutRef.current ); + return; + } + + // A timeout is being used to delay resetting the offset in cases + // where the focus is changed to a different TextInput. + clearTimeout( timeoutRef.current ); + timeoutRef.current = setTimeout( () => { + setKeyboardOffset( 0 ); + }, 500 ); + }, [ shouldPreventAutomaticScroll ] ); const onKeyboardDidShow = useCallback( ( { endCoordinates } ) => { + clearTimeout( timeoutRef.current ); setKeyboardOffset( endCoordinates.height ); }, [] ); - const onKeyboardDidHide = useCallback( () => { - setKeyboardOffset( 0 ); - }, [] ); - useEffect( () => { let showSubscription; let hideSubscription; @@ -46,6 +62,7 @@ export default function useKeyboardOffset( scrollEnabled ) { } return () => { + clearTimeout( timeoutRef.current ); showSubscription?.remove(); hideSubscription?.remove(); }; From c37687b4430d36f4b14fb946973acabbf8e3cb8f Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 3 Apr 2023 17:22:31 +0200 Subject: [PATCH 40/48] Mobile - useScrollToTextInput: Remove dash --- .../use-scroll-to-text-input.native.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js index abf34d58148f2b..3bdaba837a60b3 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js @@ -39,10 +39,10 @@ export default function useScrollToTextInput( /** * Function to scroll to the current TextInput's offset. * - * @param {Object} caret - The caret position data of the currently focused TextInput. - * @param {number} caret.caretHeight - The height of the caret. - * @param {number} textInputOffset - The offset calculated with the caret's Y coordinate + the - * - TextInput's Y coord or height value. + * @param {Object} caret The caret position data of the currently focused TextInput. + * @param {number} caret.caretHeight The height of the caret. + * @param {number} textInputOffset The offset calculated with the caret's Y coordinate + the + * TextInput's Y coord or height value. */ const scrollToTextInputOffset = useCallback( ( caret, textInputOffset ) => { From 3ff3fd45a05629f82d70f4667a2c832912708d0e Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 3 Apr 2023 17:28:13 +0200 Subject: [PATCH 41/48] Mobile - KeyboardAwareFlatList: Remove dashes --- .../mobile/keyboard-aware-flat-list/index.ios.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 7cfedf139dd370..90fda81d05b2f6 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -28,14 +28,14 @@ const AnimatedScrollView = Animated.createAnimatedComponent( ScrollView ); * React component that provides a FlatList that is aware of the keyboard state and can scroll * to the currently focused TextInput. * - * @param {Object} props - Component props. - * @param {number} props.extraScrollHeight - Extra scroll height for the content. - * @param {Function} props.innerRef - Function to pass the ScrollView ref to the parent component. - * @param {Function} props.onScroll - Function to be called when the list is scrolled. - * @param {boolean} props.scrollEnabled - Whether the list can be scrolled. - * @param {Object} props.scrollViewStyle - Additional style for the ScrollView component. - * @param {boolean} props.shouldPreventAutomaticScroll - Whether to prevent scrolling when there's a Keyboard offset set. - * @param {Object} props... - Other props to pass to the FlatList component. + * @param {Object} props Component props. + * @param {number} props.extraScrollHeight Extra scroll height for the content. + * @param {Function} props.innerRef Function to pass the ScrollView ref to the parent component. + * @param {Function} props.onScroll Function to be called when the list is scrolled. + * @param {boolean} props.scrollEnabled Whether the list can be scrolled. + * @param {Object} props.scrollViewStyle Additional style for the ScrollView component. + * @param {boolean} props.shouldPreventAutomaticScroll Whether to prevent scrolling when there's a Keyboard offset set. + * @param {Object} props... Other props to pass to the FlatList component. * @return {WPComponent} KeyboardAwareFlatList component. */ export const KeyboardAwareFlatList = ( { From 0572495ab6ca627f1537c2c0b817ac5915243145 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 4 Apr 2023 12:59:37 +0200 Subject: [PATCH 42/48] Mobile - useKeyboardOffset - Reset timeout when willShowSubscription is called --- .../use-keyboard-offset.native.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js index f3a5c386d59048..f12b254dd9469b 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-keyboard-offset.native.js @@ -35,7 +35,7 @@ export default function useKeyboardOffset( clearTimeout( timeoutRef.current ); timeoutRef.current = setTimeout( () => { setKeyboardOffset( 0 ); - }, 500 ); + }, 200 ); }, [ shouldPreventAutomaticScroll ] ); const onKeyboardDidShow = useCallback( ( { endCoordinates } ) => { @@ -43,11 +43,20 @@ export default function useKeyboardOffset( setKeyboardOffset( endCoordinates.height ); }, [] ); + const onKeyboardWillShow = useCallback( () => { + clearTimeout( timeoutRef.current ); + }, [] ); + useEffect( () => { + let willShowSubscription; let showSubscription; let hideSubscription; if ( scrollEnabled ) { + willShowSubscription = Keyboard.addListener( + 'keyboardWillShow', + onKeyboardWillShow + ); showSubscription = Keyboard.addListener( 'keyboardDidShow', onKeyboardDidShow @@ -57,15 +66,22 @@ export default function useKeyboardOffset( onKeyboardDidHide ); } else { + willShowSubscription?.remove(); showSubscription?.remove(); hideSubscription?.remove(); } return () => { clearTimeout( timeoutRef.current ); + willShowSubscription?.remove(); showSubscription?.remove(); hideSubscription?.remove(); }; - }, [ scrollEnabled, onKeyboardDidShow, onKeyboardDidHide ] ); + }, [ + onKeyboardDidHide, + onKeyboardDidShow, + onKeyboardWillShow, + scrollEnabled, + ] ); return [ keyboardOffset ]; } From 20c9ab03deb8e3bd68d37a56f7ec8dcb041dff4e Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 10 Apr 2023 11:09:05 +0200 Subject: [PATCH 43/48] Mobile - useTextInputOffset - Add example when a caretY value would be -1 --- .../keyboard-aware-flat-list/use-text-input-offset.native.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js index 9c6d3207afa9bb..ed545db7d42abc 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js @@ -32,7 +32,9 @@ export default function useTextInputOffset( scrollEnabled, scrollViewRef ) { ( _x, y, _width, height ) => { const caretYOffset = // For cases where the caretY value is -1 - // we use the y + height value. + // we use the y + height value, e.g the current + // character index is not valid or out of bounds + // see https://github.com/wordpress-mobile/AztecEditor-iOS/blob/develop/Aztec/Classes/TextKit/TextView.swift#L762 caretY >= 0 && caretY < height ? y + caretY : y + height; From 8fba7cbab4084740cde5a37b3ab8cf3278e88533 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 10 Apr 2023 11:27:12 +0200 Subject: [PATCH 44/48] Mobile - useKeyboardOffset - Remove duplicated test and use a different keyboard height end coordinates to check different offset value --- .../test/use-keyboard-offset.native.js | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js index f3bca4583260da..03a0f05399d8f9 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js @@ -121,28 +121,6 @@ describe( 'useKeyboardOffset', () => { ); } ); - it( 'sets keyboard offset to 0 when keyboard is hidden and shouldPreventAutomaticScroll is false', () => { - // Arrange - const shouldPreventAutomaticScroll = jest.fn().mockReturnValue( false ); - const { result } = renderHook( () => - useKeyboardOffset( true, shouldPreventAutomaticScroll ) - ); - - // Act - act( () => { - RCTDeviceEventEmitter.emit( 'keyboardDidShow', { - endCoordinates: { height: 250 }, - } ); - } ); - act( () => { - RCTDeviceEventEmitter.emit( 'keyboardDidHide' ); - jest.runAllTimers(); - } ); - - // Assert - expect( result.current[ 0 ] ).toBe( 0 ); - } ); - it( 'does not set keyboard offset to 0 when keyboard is hidden and shouldPreventAutomaticScroll is true', () => { // Arrange const shouldPreventAutomaticScroll = jest.fn().mockReturnValue( true ); @@ -187,12 +165,12 @@ describe( 'useKeyboardOffset', () => { // Act act( () => { RCTDeviceEventEmitter.emit( 'keyboardDidShow', { - endCoordinates: { height: 250 }, + endCoordinates: { height: 150 }, } ); } ); // Assert - expect( result.current[ 0 ] ).toBe( 250 ); + expect( result.current[ 0 ] ).toBe( 150 ); // Act act( () => { @@ -205,7 +183,7 @@ describe( 'useKeyboardOffset', () => { } ); // Assert - expect( result.current[ 0 ] ).toBe( 250 ); + expect( result.current[ 0 ] ).toBe( 150 ); // Act act( () => { From 9793ad0b0498920dbdcd36ff2686ec4d6d211e75 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 10 Apr 2023 11:28:58 +0200 Subject: [PATCH 45/48] Mobile - useKeyboardOffset - Update test to also remove the keyboardWillShow listener --- .../keyboard-aware-flat-list/test/use-keyboard-offset.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js index 03a0f05399d8f9..18265682b305a5 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-keyboard-offset.native.js @@ -16,6 +16,7 @@ describe( 'useKeyboardOffset', () => { beforeEach( () => { Keyboard.removeAllListeners( 'keyboardDidShow' ); Keyboard.removeAllListeners( 'keyboardDidHide' ); + Keyboard.removeAllListeners( 'keyboardWillShow' ); } ); it( 'returns the initial state', () => { From 6e7556f34cb9094f831a56def91cae70f64dce77 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 10 Apr 2023 11:57:25 +0200 Subject: [PATCH 46/48] Mobile - Update Changelog --- 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 e425af19ec0890..1eedaf1467ddcd 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Support POST requests [#49371] - [*] Avoid empty Gallery block error [#49557] +- [***] [iOS] Fixed iOS scroll jumping issue by refactoring KeyboardAwareFlatList improving writing flow and caret focus handling. [#48791] ## 1.92.0 * No User facing changes * From a5190853311cbb97ae052bb18af16daf0205cdfd Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 10 Apr 2023 12:32:48 +0200 Subject: [PATCH 47/48] Components - Update changelog to include the KeyboardAwareFlatList mobile refactor --- packages/components/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index bb5597178f0de9..49d10aacb9fb1b 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internal + +- `Mobile` Refactor of the KeyboardAwareFlatList component. + ### Enhancements - `DropZone`: Smooth animation ([#49517](https://github.com/WordPress/gutenberg/pull/49517)). From a0806745f4b958e7e84802b4390570b7c2f472b8 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 10 Apr 2023 13:07:59 +0200 Subject: [PATCH 48/48] Mobile - useTextInputOffset - Update comment to use permanent link --- .../keyboard-aware-flat-list/use-text-input-offset.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js index ed545db7d42abc..1c69cbcc48c45f 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-text-input-offset.native.js @@ -34,7 +34,7 @@ export default function useTextInputOffset( scrollEnabled, scrollViewRef ) { // For cases where the caretY value is -1 // we use the y + height value, e.g the current // character index is not valid or out of bounds - // see https://github.com/wordpress-mobile/AztecEditor-iOS/blob/develop/Aztec/Classes/TextKit/TextView.swift#L762 + // see https://github.com/wordpress-mobile/AztecEditor-iOS/blob/4d0522d67b0056ac211466caaa76936cc5b4f947/Aztec/Classes/TextKit/TextView.swift#L762 caretY >= 0 && caretY < height ? y + caretY : y + height;