Skip to content

Commit

Permalink
Let ScrollView Know About Keyboard Opened Before Mount
Browse files Browse the repository at this point in the history
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
  • Loading branch information
NickGerleman authored and kelset committed Oct 3, 2022
1 parent 75d5679 commit 764f584
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 21 deletions.
31 changes: 28 additions & 3 deletions Libraries/Components/Keyboard/Keyboard.js
Expand Up @@ -24,7 +24,7 @@ export type KeyboardEventEasing =
| 'linear'
| 'keyboard';

export type KeyboardEventCoordinates = $ReadOnly<{|
export type KeyboardMetrics = $ReadOnly<{|
screenX: number,
screenY: number,
width: number,
Expand All @@ -36,7 +36,7 @@ export type KeyboardEvent = AndroidKeyboardEvent | IOSKeyboardEvent;
type BaseKeyboardEvent = {|
duration: number,
easing: KeyboardEventEasing,
endCoordinates: KeyboardEventCoordinates,
endCoordinates: KeyboardMetrics,
|};

export type AndroidKeyboardEvent = $ReadOnly<{|
Expand All @@ -47,7 +47,7 @@ export type AndroidKeyboardEvent = $ReadOnly<{|

export type IOSKeyboardEvent = $ReadOnly<{|
...BaseKeyboardEvent,
startCoordinates: KeyboardEventCoordinates,
startCoordinates: KeyboardMetrics,
isEventFromThisApp: boolean,
|}>;

Expand Down Expand Up @@ -103,13 +103,24 @@ type KeyboardEventDefinitions = {
*/

class Keyboard {
_currentlyShowing: ?KeyboardEvent;

_emitter: NativeEventEmitter<KeyboardEventDefinitions> =
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
// If you want to use the native module on other platforms, please remove this condition and test its behavior
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.
Expand Down Expand Up @@ -157,6 +168,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.
Expand Down
4 changes: 2 additions & 2 deletions Libraries/Components/Keyboard/KeyboardAvoidingView.js
Expand Up @@ -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,
Expand Down Expand Up @@ -71,7 +71,7 @@ class KeyboardAvoidingView extends React.Component<Props, State> {
this.viewRef = React.createRef();
}

_relativeKeyboardHeight(keyboardFrame: KeyboardEventCoordinates): number {
_relativeKeyboardHeight(keyboardFrame: KeyboardMetrics): number {
const frame = this._frame;
if (!frame || !keyboardFrame) {
return 0;
Expand Down
28 changes: 12 additions & 16 deletions Libraries/Components/ScrollView/ScrollView.js
Expand Up @@ -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';
Expand Down Expand Up @@ -731,7 +731,7 @@ class ScrollView extends React.Component<Props, State> {
new Map();
_headerLayoutYs: Map<string, number> = new Map();

_keyboardWillOpenTo: ?KeyboardEvent = null;
_keyboardMetrics: ?KeyboardMetrics = null;
_additionalScrollOffset: number = 0;
_isTouching: boolean = false;
_lastMomentumScrollBeginTime: number = 0;
Expand Down Expand Up @@ -769,7 +769,7 @@ class ScrollView extends React.Component<Props, State> {
);
}

this._keyboardWillOpenTo = null;
this._keyboardMetrics = Keyboard.metrics();
this._additionalScrollOffset = 0;

this._subscriptionKeyboardWillShow = Keyboard.addListener(
Expand Down Expand Up @@ -1075,8 +1075,8 @@ class ScrollView extends React.Component<Props, State> {
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;
Expand All @@ -1094,8 +1094,8 @@ class ScrollView extends React.Component<Props, State> {
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.
Expand Down Expand Up @@ -1243,32 +1243,28 @@ class ScrollView extends React.Component<Props, State> {
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);
};

Expand Down Expand Up @@ -1547,7 +1543,7 @@ class ScrollView extends React.Component<Props, State> {
// 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;
};
Expand Down

0 comments on commit 764f584

Please sign in to comment.