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 facebook-github-bot committed Aug 18, 2022
1 parent 9e4114a commit 26d1480
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 @@ -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.
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

3 comments on commit 26d1480

@idrissakhi
Copy link

Choose a reason for hiding this comment

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

This update is causing issues when automaticallyAdjustKeyboardInsets is used on ScrollViews with inputAccessoryView to display static input (only versions below 16, works fine on iOS 16)

@NickGerleman
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This update is causing issues when automaticallyAdjustKeyboardInsets is used on ScrollViews with inputAccessoryView to display static input (only versions below 16, works fine on iOS 16)

Hmm, the implementation for automaticallyAdjustKeyboardInsets looks to all be in native. I can't see how it would depend on these values. What led you to this specific change as causing the issue?

@idrissakhi
Copy link

Choose a reason for hiding this comment

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

if i downgrade version to 0.70 everything works fine. And that’s the only change that I see which could affect scrollView

Please sign in to comment.