Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ios: Fix app-unusable bug with "Prefer Cross-Fade Transitions" setting #5163

Merged
merged 4 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .eslintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,19 @@ overrides:
# a few variables already declared in upper scope; no bugs known
# to result from this
no-shadow: off

#
# ================================================================
# Third-party code: react-native.
#
# We leave this code in the style we received it in.
- files: ['src/third/react-native/**']
rules:
strict: off
import/order: off
import/first: off
object-curly-spacing: off
react/state-in-constructor: off
no-unused-vars: off
no-case-declarations: off
react/jsx-closing-bracket-location: off
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ flow-typed/@react-navigation
# These are purely type definitions, no runtime code. Most of them
# are third-party code, too, so naturally don't match our style.
**/flow-typed/**

# Third-party code: react-native. We leave this code in the style we
# received it in.
src/third/react-native
23 changes: 18 additions & 5 deletions docs/THIRDPARTY
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,29 @@ Comment:
Kandra Labs of any inaccurate information or errors found in these notices.

Files: *
Copyright: 2016-2018 Kandra Labs, Inc., and contributors
Copyright: 2016-2021 Kandra Labs, Inc., and contributors
License: Apache-2

Files: src/redux-persist-migrate/index.js
Copyright: 2016-2017 Zack Story and contributors
License: MIT
Copyright: 2016-2017 Zack Story and contributors, 2018-2021 Kandra Labs, Inc.,
and contributors
License: Apache-2
Comment:
Upstream is licensed as MIT.

Files: vendor/intl/*
Copyright: 2013 Andy Earnshaw
Files: src/third/react-native/*
Copyright: Facebook, Inc., and its affiliates
License: MIT
Comment:
Changes we make here are designed to be sent upstream. So even when making
changes, we keep our versions on the MIT license, the same as upstream.

Files: src/third/redux-persist/*
Copyright: 2015-2017 Zack Story, 2020-2021 Kandra Labs, Inc., and contributors
License: Apache-2
Comment:
Upstream is licensed as MIT. Forked from v4.10.2, at
https://github.com/rt2zz/redux-persist/tree/f4a6e86c666 .

Files: static/img/app-store-badge.*
Copyright: 2018 Apple Inc.
Expand Down
4 changes: 3 additions & 1 deletion src/common/KeyboardAvoider.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* @flow strict-local */
import React, { PureComponent } from 'react';
import type { Node } from 'react';
import { KeyboardAvoidingView, Platform, View } from 'react-native';
import { Platform, View } from 'react-native';
import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';

import KeyboardAvoidingView from '../third/react-native/KeyboardAvoidingView';

type Props = $ReadOnly<{|
behavior?: ?('height' | 'position' | 'padding'),
children: Node,
Expand Down
245 changes: 245 additions & 0 deletions src/third/react-native/KeyboardAvoidingView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* docs/THIRDPARTY file from the root directory of this source tree.
*
* @format
* @flow strict-local
*/

'use strict';

import React from 'react';
import type { ElementRef, Node } from 'react';
import { Keyboard, LayoutAnimation, Platform, StyleSheet, View } from 'react-native';
import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';
import type { EventSubscription } from 'react-native/Libraries/vendor/emitter/EventEmitter';
import type { KeyboardEvent } from 'react-native/Libraries/Components/Keyboard/Keyboard';
import type {
ViewProps,
ViewLayout,
ViewLayoutEvent,
} from 'react-native/Libraries/Components/View/ViewPropTypes';

type Props = $ReadOnly<{|
...ViewProps,

/**
* Specify how to react to the presence of the keyboard.
*/
behavior?: ?('height' | 'position' | 'padding'),

/**
* Style of the content container when `behavior` is 'position'.
*/
contentContainerStyle?: ?ViewStyleProp,

/**
* Controls whether this `KeyboardAvoidingView` instance should take effect.
* This is useful when more than one is on the screen. Defaults to true.
*/
enabled: ?boolean,

/**
* Distance between the top of the user screen and the React Native view. This
* may be non-zero in some cases. Defaults to 0.
*/
keyboardVerticalOffset: number,
|}>;

type State = {|
bottom: number,
|};

/**
* View that moves out of the way when the keyboard appears by automatically
* adjusting its height, position, or bottom padding.
*/
class KeyboardAvoidingView extends React.Component<Props, State> {
static defaultProps: {|enabled: boolean, keyboardVerticalOffset: number|} = {
enabled: true,
keyboardVerticalOffset: 0,
};

_frame: ?ViewLayout = null;
_keyboardEvent: ?KeyboardEvent = null;
_subscriptions: Array<EventSubscription> = [];
// $FlowFixMe[unclear-type]
viewRef: {current: ElementRef<any> | null, ...};
_initialFrameHeight: number = 0;

constructor(props: Props) {
super(props);
this.state = {bottom: 0};
this.viewRef = React.createRef();
}

_relativeKeyboardHeight(keyboardFrame): number {
const frame = this._frame;
if (!frame || !keyboardFrame) {
return 0;
}

if (keyboardFrame.height === 0) {
// The keyboard is hidden, and occupies no height.
//
// This condition is needed because in some circumstances when the
// keyboard is hidden we get both `screenY` and `height` (as well as
// `screenX` and `width`) of zero. In particular, this happens on iOS
// when the UIAccessibilityPrefersCrossFadeTransitions setting is
// true, i.e. when the user has enabled both "Reduce Motion" and
// "Prefer Cross-Fade Transitions" under Settings > Accessibility >
// Motion. See zulip/zulip-mobile#5162 and
// facebook/react-native#29974.
return 0;
}

const keyboardY = keyboardFrame.screenY - this.props.keyboardVerticalOffset;

// Calculate the displacement needed for the view such that it
// no longer overlaps with the keyboard
return Math.max(frame.y + frame.height - keyboardY, 0);
}

_onKeyboardChange = (event: ?KeyboardEvent) => {
this._keyboardEvent = event;
this._updateBottomIfNecesarry();
};

_onLayout = (event: ViewLayoutEvent) => {
const wasFrameNull = this._frame == null;
this._frame = event.nativeEvent.layout;
if (!this._initialFrameHeight) {
// save the initial frame height, before the keyboard is visible
this._initialFrameHeight = this._frame.height;
}

if (wasFrameNull) {
this._updateBottomIfNecesarry();
}
};

_updateBottomIfNecesarry = () => {
if (this._keyboardEvent == null) {
this.setState({bottom: 0});
return;
}

const {duration, easing, endCoordinates} = this._keyboardEvent;
const height = this._relativeKeyboardHeight(endCoordinates);

if (this.state.bottom === height) {
return;
}

// $FlowFixMe[sketchy-number-and]
if (duration && easing) {
LayoutAnimation.configureNext({
// We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m
duration: duration > 10 ? duration : 10,
update: {
duration: duration > 10 ? duration : 10,
type: LayoutAnimation.Types[easing] || 'keyboard',
},
});
}
this.setState({bottom: height});
};

componentDidMount(): void {
if (Platform.OS === 'ios') {
this._subscriptions = [
Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange),
];
} else {
this._subscriptions = [
Keyboard.addListener('keyboardDidHide', this._onKeyboardChange),
Keyboard.addListener('keyboardDidShow', this._onKeyboardChange),
];
}
}

componentWillUnmount(): void {
this._subscriptions.forEach(subscription => {
subscription.remove();
});
}

render(): Node {
const {
behavior,
children,
contentContainerStyle,
enabled,
keyboardVerticalOffset,
style,
...props
} = this.props;
// $FlowFixMe[sketchy-null-bool]
const bottomHeight = enabled ? this.state.bottom : 0;
switch (behavior) {
case 'height':
let heightStyle;
if (this._frame != null && this.state.bottom > 0) {
// Note that we only apply a height change when there is keyboard present,
// i.e. this.state.bottom is greater than 0. If we remove that condition,
// this.frame.height will never go back to its original value.
// When height changes, we need to disable flex.
heightStyle = {
height: this._initialFrameHeight - bottomHeight,
flex: 0,
};
}
return (
<View
ref={this.viewRef}
style={StyleSheet.compose(style, heightStyle)}
onLayout={this._onLayout}
{...props}>
{children}
</View>
);

case 'position':
return (
<View
ref={this.viewRef}
style={style}
onLayout={this._onLayout}
{...props}>
<View
style={StyleSheet.compose(contentContainerStyle, {
bottom: bottomHeight,
})}>
{children}
</View>
</View>
);

case 'padding':
return (
<View
ref={this.viewRef}
style={StyleSheet.compose(style, {paddingBottom: bottomHeight})}
onLayout={this._onLayout}
{...props}>
{children}
</View>
);

default:
return (
<View
ref={this.viewRef}
onLayout={this._onLayout}
style={style}
{...props}>
{children}
</View>
);
}
}
}

module.exports = KeyboardAvoidingView;