From 26d148029c7fde117f33b0d6c8b34286c45a0ef2 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Thu, 18 Aug 2022 15:05:43 -0700 Subject: [PATCH] Let ScrollView Know About Keyboard Opened Before Mount Summary: ScrollView has special behavior when the keyboard is open, but starts listening to keyboard events on mount. This means a ScrollView mounted after the keyboard is already up (e.g. for a typeahead) is not initialized to the keyboard being up. This change adds `Keyboard.isVisible()` and `Keyboard.metrics()` APIs to allow seeding initial keyboard metrics. Changelog: [General][Fixed] - Inform ScrollView of Keyboard Events Before Mount Reviewed By: JoshuaGross, yungsters Differential Revision: D38701976 fbshipit-source-id: 42b354718fbf5001ca4b90de0442eeab0be91e7a --- Libraries/Components/Keyboard/Keyboard.js | 31 +++++++++++++++++-- .../Keyboard/KeyboardAvoidingView.js | 4 +-- Libraries/Components/ScrollView/ScrollView.js | 28 +++++++---------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/Libraries/Components/Keyboard/Keyboard.js b/Libraries/Components/Keyboard/Keyboard.js index d80f9356779033..8557028b3f4c61 100644 --- a/Libraries/Components/Keyboard/Keyboard.js +++ b/Libraries/Components/Keyboard/Keyboard.js @@ -24,7 +24,7 @@ export type KeyboardEventEasing = | 'linear' | 'keyboard'; -export type KeyboardEventCoordinates = $ReadOnly<{| +export type KeyboardMetrics = $ReadOnly<{| screenX: number, screenY: number, width: number, @@ -36,7 +36,7 @@ export type KeyboardEvent = AndroidKeyboardEvent | IOSKeyboardEvent; type BaseKeyboardEvent = {| duration: number, easing: KeyboardEventEasing, - endCoordinates: KeyboardEventCoordinates, + endCoordinates: KeyboardMetrics, |}; export type AndroidKeyboardEvent = $ReadOnly<{| @@ -47,7 +47,7 @@ export type AndroidKeyboardEvent = $ReadOnly<{| export type IOSKeyboardEvent = $ReadOnly<{| ...BaseKeyboardEvent, - startCoordinates: KeyboardEventCoordinates, + startCoordinates: KeyboardMetrics, isEventFromThisApp: boolean, |}>; @@ -103,6 +103,8 @@ type KeyboardEventDefinitions = { */ class Keyboard { + _currentlyShowing: ?KeyboardEvent; + _emitter: NativeEventEmitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior @@ -110,6 +112,15 @@ class Keyboard { Platform.OS !== 'ios' ? null : NativeKeyboardObserver, ); + constructor() { + this.addListener('keyboardDidShow', ev => { + this._currentlyShowing = ev; + }); + this.addListener('keyboardDidHide', _ev => { + this._currentlyShowing = null; + }); + } + /** * The `addListener` function connects a JavaScript function to an identified native * keyboard notification event. @@ -158,6 +169,20 @@ class Keyboard { dismissKeyboard(); } + /** + * Whether the keyboard is last known to be visible. + */ + isVisible(): boolean { + return !!this._currentlyShowing; + } + + /** + * Return the metrics of the soft-keyboard if visible. + */ + metrics(): ?KeyboardMetrics { + return this._currentlyShowing?.endCoordinates; + } + /** * Useful for syncing TextInput (or other keyboard accessory view) size of * position changes with keyboard movements. diff --git a/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/Libraries/Components/Keyboard/KeyboardAvoidingView.js index 4536402c5104f6..ebbb16e4212e22 100644 --- a/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -22,7 +22,7 @@ import type { ViewLayout, ViewLayoutEvent, } from '../View/ViewPropTypes'; -import type {KeyboardEvent, KeyboardEventCoordinates} from './Keyboard'; +import type {KeyboardEvent, KeyboardMetrics} from './Keyboard'; type Props = $ReadOnly<{| ...ViewProps, @@ -71,7 +71,7 @@ class KeyboardAvoidingView extends React.Component { this.viewRef = React.createRef(); } - _relativeKeyboardHeight(keyboardFrame: KeyboardEventCoordinates): number { + _relativeKeyboardHeight(keyboardFrame: KeyboardMetrics): number { const frame = this._frame; if (!frame || !keyboardFrame) { return 0; diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 1d494f228bfbbe..768b48c6b242d2 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -42,7 +42,7 @@ import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; import type {ViewProps} from '../View/ViewPropTypes'; import ScrollViewContext, {HORIZONTAL, VERTICAL} from './ScrollViewContext'; import type {Props as ScrollViewStickyHeaderProps} from './ScrollViewStickyHeader'; -import type {KeyboardEvent} from '../Keyboard/Keyboard'; +import type {KeyboardEvent, KeyboardMetrics} from '../Keyboard/Keyboard'; import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; import Commands from './ScrollViewCommands'; @@ -731,7 +731,7 @@ class ScrollView extends React.Component { new Map(); _headerLayoutYs: Map = new Map(); - _keyboardWillOpenTo: ?KeyboardEvent = null; + _keyboardMetrics: ?KeyboardMetrics = null; _additionalScrollOffset: number = 0; _isTouching: boolean = false; _lastMomentumScrollBeginTime: number = 0; @@ -769,7 +769,7 @@ class ScrollView extends React.Component { ); } - this._keyboardWillOpenTo = null; + this._keyboardMetrics = Keyboard.metrics(); this._additionalScrollOffset = 0; this._subscriptionKeyboardWillShow = Keyboard.addListener( @@ -1075,8 +1075,8 @@ class ScrollView extends React.Component { let keyboardScreenY = Dimensions.get('window').height; const scrollTextInputIntoVisibleRect = () => { - if (this._keyboardWillOpenTo != null) { - keyboardScreenY = this._keyboardWillOpenTo.endCoordinates.screenY; + if (this._keyboardMetrics != null) { + keyboardScreenY = this._keyboardMetrics.screenY; } let scrollOffsetY = top - keyboardScreenY + height + this._additionalScrollOffset; @@ -1094,8 +1094,8 @@ class ScrollView extends React.Component { this._preventNegativeScrollOffset = false; }; - if (this._keyboardWillOpenTo == null) { - // `_keyboardWillOpenTo` is set inside `scrollResponderKeyboardWillShow` which + if (this._keyboardMetrics == null) { + // `_keyboardMetrics` is set inside `scrollResponderKeyboardWillShow` which // is not guaranteed to be called before `_inputMeasureAndScrollToKeyboard` but native has already scheduled it. // In case it was not called before `_inputMeasureAndScrollToKeyboard`, we postpone scrolling to // text input. @@ -1243,32 +1243,28 @@ class ScrollView extends React.Component { scrollResponderKeyboardWillShow: (e: KeyboardEvent) => void = ( e: KeyboardEvent, ) => { - this._keyboardWillOpenTo = e; + this._keyboardMetrics = e.endCoordinates; this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e); }; scrollResponderKeyboardWillHide: (e: KeyboardEvent) => void = ( e: KeyboardEvent, ) => { - this._keyboardWillOpenTo = null; + this._keyboardMetrics = null; this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e); }; scrollResponderKeyboardDidShow: (e: KeyboardEvent) => void = ( e: KeyboardEvent, ) => { - // TODO(7693961): The event for DidShow is not available on iOS yet. - // Use the one from WillShow and do not assign. - if (e) { - this._keyboardWillOpenTo = e; - } + this._keyboardMetrics = e.endCoordinates; this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e); }; scrollResponderKeyboardDidHide: (e: KeyboardEvent) => void = ( e: KeyboardEvent, ) => { - this._keyboardWillOpenTo = null; + this._keyboardMetrics = null; this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e); }; @@ -1547,7 +1543,7 @@ class ScrollView extends React.Component { // keyboard, except on Android where setting windowSoftInputMode to // adjustNone leads to missing keyboard events. const softKeyboardMayBeOpen = - this._keyboardWillOpenTo != null || Platform.OS === 'android'; + this._keyboardMetrics != null || Platform.OS === 'android'; return hasFocusedTextInput && softKeyboardMayBeOpen; };