diff --git a/src/components/ExpensiTextInput/BaseExpensiTextInput.js b/src/components/ExpensiTextInput/BaseExpensiTextInput.js
index c5fcd6b3d42c..9394a8d9f88d 100644
--- a/src/components/ExpensiTextInput/BaseExpensiTextInput.js
+++ b/src/components/ExpensiTextInput/BaseExpensiTextInput.js
@@ -1,7 +1,7 @@
import _ from 'underscore';
import React, {Component} from 'react';
import {
- Animated, TextInput, View, TouchableWithoutFeedback, Pressable,
+ Animated, View, TouchableWithoutFeedback, Pressable,
} from 'react-native';
import Str from 'expensify-common/lib/str';
import ExpensiTextInputLabel from './ExpensiTextInputLabel';
@@ -12,6 +12,7 @@ import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import InlineErrorText from '../InlineErrorText';
import * as styleConst from './styleConst';
+import TextInputWithName from '../TextInputWithName';
class BaseExpensiTextInput extends Component {
constructor(props) {
@@ -167,11 +168,12 @@ class BaseExpensiTextInput extends Component {
label={this.props.label}
labelTranslateY={this.state.labelTranslateY}
labelScale={this.state.labelScale}
+ for={this.props.nativeID}
/>
>
) : null}
- {
if (typeof this.props.innerRef === 'function') { this.props.innerRef(ref); }
this.input = ref;
@@ -189,18 +191,19 @@ class BaseExpensiTextInput extends Component {
onChangeText={this.setValue}
secureTextEntry={this.state.passwordHidden}
onPressOut={this.props.onPress}
+ name={this.props.name}
/>
{this.props.secureTextEntry && (
-
-
-
+
+
+
)}
diff --git a/src/components/ExpensiTextInput/ExpensiTextInputLabel/expensiTextInputLabelPropTypes.js b/src/components/ExpensiTextInput/ExpensiTextInputLabel/expensiTextInputLabelPropTypes.js
index 3de0e9bbfe78..775605b93fc5 100644
--- a/src/components/ExpensiTextInput/ExpensiTextInputLabel/expensiTextInputLabelPropTypes.js
+++ b/src/components/ExpensiTextInput/ExpensiTextInputLabel/expensiTextInputLabelPropTypes.js
@@ -3,13 +3,23 @@ import {Animated} from 'react-native';
const propTypes = {
/** Label */
- label: PropTypes.string,
+ label: PropTypes.string.isRequired,
/** Label vertical translate */
labelTranslateY: PropTypes.instanceOf(Animated.Value).isRequired,
/** Label scale */
labelScale: PropTypes.instanceOf(Animated.Value).isRequired,
+
+ /** For attribute for label */
+ for: PropTypes.string,
+};
+
+const defaultProps = {
+ for: '',
};
-export default propTypes;
+export {
+ propTypes,
+ defaultProps,
+};
diff --git a/src/components/ExpensiTextInput/ExpensiTextInputLabel/index.js b/src/components/ExpensiTextInput/ExpensiTextInputLabel/index.js
index 6117714370a4..208f6271be9e 100644
--- a/src/components/ExpensiTextInput/ExpensiTextInputLabel/index.js
+++ b/src/components/ExpensiTextInput/ExpensiTextInputLabel/index.js
@@ -1,26 +1,39 @@
-import React, {memo} from 'react';
+import React, {PureComponent} from 'react';
import {Animated} from 'react-native';
import styles from '../../../styles/styles';
-import propTypes from './expensiTextInputLabelPropTypes';
+import {propTypes, defaultProps} from './expensiTextInputLabelPropTypes';
-const ExpensiTextInputLabel = props => (
-
- {props.label}
-
-);
+class ExpensiTextInputLabel extends PureComponent {
+ componentDidMount() {
+ if (!this.props.for) {
+ return;
+ }
+ this.label.setNativeProps({for: this.props.for});
+ }
+
+ render() {
+ return (
+ this.label = el}
+ style={[
+ styles.expensiTextInputLabel,
+ styles.expensiTextInputLabelDesktop,
+ styles.expensiTextInputLabelTransformation(
+ this.props.labelTranslateY,
+ 0,
+ this.props.labelScale,
+ ),
+ ]}
+ >
+ {this.props.label}
+
+ );
+ }
+}
ExpensiTextInputLabel.propTypes = propTypes;
-ExpensiTextInputLabel.displayName = 'ExpensiTextInputLabel';
+ExpensiTextInputLabel.defaultProps = defaultProps;
-export default memo(ExpensiTextInputLabel);
+export default ExpensiTextInputLabel;
diff --git a/src/components/ExpensiTextInput/ExpensiTextInputLabel/index.native.js b/src/components/ExpensiTextInput/ExpensiTextInputLabel/index.native.js
index e249f89e1b52..b2c2abcff185 100644
--- a/src/components/ExpensiTextInput/ExpensiTextInputLabel/index.native.js
+++ b/src/components/ExpensiTextInput/ExpensiTextInputLabel/index.native.js
@@ -1,7 +1,7 @@
import React, {PureComponent} from 'react';
import {Animated} from 'react-native';
import styles from '../../../styles/styles';
-import propTypes from './expensiTextInputLabelPropTypes';
+import * as expensiTextInputLabelPropTypes from './expensiTextInputLabelPropTypes';
import * as styleConst from '../styleConst';
class ExpensiTextInputLabel extends PureComponent {
@@ -36,6 +36,7 @@ class ExpensiTextInputLabel extends PureComponent {
}
}
-ExpensiTextInputLabel.propTypes = propTypes;
+ExpensiTextInputLabel.propTypes = expensiTextInputLabelPropTypes.propTypes;
+ExpensiTextInputLabel.defaultProps = expensiTextInputLabelPropTypes.defaultProps;
export default ExpensiTextInputLabel;
diff --git a/src/components/ExpensiTextInput/baseExpensiTextInputPropTypes.js b/src/components/ExpensiTextInput/baseExpensiTextInputPropTypes.js
index f3cdbae2ca94..6023b6d31d9c 100644
--- a/src/components/ExpensiTextInput/baseExpensiTextInputPropTypes.js
+++ b/src/components/ExpensiTextInput/baseExpensiTextInputPropTypes.js
@@ -4,6 +4,9 @@ const propTypes = {
/** Input label */
label: PropTypes.string,
+ /** Name attribute for the input */
+ name: PropTypes.string,
+
/** Input value */
value: PropTypes.string,
@@ -33,6 +36,7 @@ const propTypes = {
const defaultProps = {
label: '',
+ name: '',
errorText: '',
placeholder: '',
hasError: false,
diff --git a/src/components/Form/BaseForm.js b/src/components/Form/BaseForm.js
new file mode 100644
index 000000000000..21a4e19b1f9b
--- /dev/null
+++ b/src/components/Form/BaseForm.js
@@ -0,0 +1,16 @@
+import React, {forwardRef} from 'react';
+import {View} from 'react-native';
+import * as ComponentUtils from '../../libs/ComponentUtils';
+
+const BaseForm = forwardRef((props, ref) => (
+
+));
+
+BaseForm.displayName = 'BaseForm';
+export default BaseForm;
diff --git a/src/components/Form/index.js b/src/components/Form/index.js
new file mode 100644
index 000000000000..163f1ee223b1
--- /dev/null
+++ b/src/components/Form/index.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import BaseForm from './BaseForm';
+
+class Form extends React.Component {
+ componentDidMount() {
+ if (!this.form) {
+ return;
+ }
+
+ // Password Managers need these attributes to be able to identify the form elements properly.
+ this.form.setNativeProps({
+ method: 'post',
+ action: '/',
+ });
+ }
+
+ render() {
+ return (
+ this.form = el}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...this.props}
+ />
+ );
+ }
+}
+
+export default Form;
diff --git a/src/components/Form/index.native.js b/src/components/Form/index.native.js
new file mode 100644
index 000000000000..21f10e7a428d
--- /dev/null
+++ b/src/components/Form/index.native.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import BaseForm from './BaseForm';
+
+// eslint-disable-next-line react/jsx-props-no-spreading
+const Form = props => ;
+
+Form.displayName = 'Form';
+export default Form;
diff --git a/src/components/TextInputWithName/index.js b/src/components/TextInputWithName/index.js
new file mode 100755
index 000000000000..5a78089af609
--- /dev/null
+++ b/src/components/TextInputWithName/index.js
@@ -0,0 +1,40 @@
+import _ from 'underscore';
+import React from 'react';
+import {TextInput} from 'react-native';
+import textInputWithNamepropTypes from './textInputWithNamepropTypes';
+
+/**
+ * On web we need to set the native attribute name for accessiblity.
+ */
+class TextInputWithName extends React.Component {
+ componentDidMount() {
+ if (!this.textInput) {
+ return;
+ }
+ if (_.isFunction(this.props.forwardedRef)) {
+ this.props.forwardedRef(this.textInput);
+ }
+
+ if (this.props.name) {
+ this.textInput.setNativeProps({name: this.props.name});
+ }
+ }
+
+ render() {
+ return (
+ this.textInput = el}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...this.props}
+ />
+ );
+ }
+}
+
+TextInputWithName.propTypes = textInputWithNamepropTypes.propTypes;
+TextInputWithName.defaultProps = textInputWithNamepropTypes.defaultProps;
+
+export default React.forwardRef((props, ref) => (
+ /* eslint-disable-next-line react/jsx-props-no-spreading */
+
+));
diff --git a/src/components/TextInputWithName/index.native.js b/src/components/TextInputWithName/index.native.js
new file mode 100644
index 000000000000..198c9540dc2f
--- /dev/null
+++ b/src/components/TextInputWithName/index.native.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import {TextInput} from 'react-native';
+import textInputWithNamepropTypes from './textInputWithNamepropTypes';
+
+const TextInputWithName = props => (
+
+);
+
+TextInputWithName.propTypes = textInputWithNamepropTypes.propTypes;
+TextInputWithName.defaultProps = textInputWithNamepropTypes.defaultProps;
+TextInputWithName.displayName = 'TextInputWithName';
+
+export default React.forwardRef((props, ref) => (
+ /* eslint-disable-next-line react/jsx-props-no-spreading */
+
+));
diff --git a/src/components/TextInputWithName/textInputWithNamepropTypes.js b/src/components/TextInputWithName/textInputWithNamepropTypes.js
new file mode 100644
index 000000000000..902ff2289d68
--- /dev/null
+++ b/src/components/TextInputWithName/textInputWithNamepropTypes.js
@@ -0,0 +1,19 @@
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ /** Name attribute for the input */
+ name: PropTypes.string,
+
+ /** A ref to forward to the text input */
+ forwardedRef: PropTypes.func,
+};
+
+const defaultProps = {
+ name: '',
+ forwardedRef: () => {},
+};
+
+export default {
+ propTypes,
+ defaultProps,
+};
diff --git a/src/components/withToggleVisibilityView.js b/src/components/withToggleVisibilityView.js
new file mode 100644
index 000000000000..3a98b09c205b
--- /dev/null
+++ b/src/components/withToggleVisibilityView.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../styles/styles';
+import getComponentDisplayName from '../libs/getComponentDisplayName';
+
+const toggleVisibilityViewPropTypes = {
+ /** Whether the content is visible. */
+ isVisible: PropTypes.bool,
+};
+
+export default function (WrappedComponent) {
+ const WithToggleVisibilityView = props => (
+
+
+
+ );
+
+ WithToggleVisibilityView.displayName = `WithToggleVisibilityView(${getComponentDisplayName(WrappedComponent)})`;
+ WithToggleVisibilityView.propTypes = {
+ forwardedRef: PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.shape({current: PropTypes.instanceOf(React.Component)}),
+ ]),
+
+ /** Whether the content is visible. */
+ isVisible: PropTypes.bool,
+ };
+ WithToggleVisibilityView.defaultProps = {
+ forwardedRef: undefined,
+ isVisible: false,
+ };
+ return React.forwardRef((props, ref) => (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+ ));
+}
+
+export {
+ toggleVisibilityViewPropTypes,
+};
diff --git a/src/libs/ComponentUtils/index.js b/src/libs/ComponentUtils/index.js
new file mode 100644
index 000000000000..f3b1b07f07d8
--- /dev/null
+++ b/src/libs/ComponentUtils/index.js
@@ -0,0 +1,10 @@
+/**
+ * Web password field needs `current-password` as autocomplete type which is not supported on native
+ */
+const PASSWORD_AUTOCOMPLETE_TYPE = 'current-password';
+const ACCESSIBILITY_ROLE_FORM = 'form';
+
+export {
+ PASSWORD_AUTOCOMPLETE_TYPE,
+ ACCESSIBILITY_ROLE_FORM,
+};
diff --git a/src/libs/ComponentUtils/index.native.js b/src/libs/ComponentUtils/index.native.js
new file mode 100644
index 000000000000..fcde8612a2f9
--- /dev/null
+++ b/src/libs/ComponentUtils/index.native.js
@@ -0,0 +1,7 @@
+const PASSWORD_AUTOCOMPLETE_TYPE = 'password';
+const ACCESSIBILITY_ROLE_FORM = 'none';
+
+export {
+ PASSWORD_AUTOCOMPLETE_TYPE,
+ ACCESSIBILITY_ROLE_FORM,
+};
diff --git a/src/pages/signin/ChangeExpensifyLoginLink.js b/src/pages/signin/ChangeExpensifyLoginLink.js
index f600438ec401..9469828a21e6 100755
--- a/src/pages/signin/ChangeExpensifyLoginLink.js
+++ b/src/pages/signin/ChangeExpensifyLoginLink.js
@@ -16,11 +16,17 @@ const propTypes = {
credentials: PropTypes.shape({
/** The email the user logged in with */
login: PropTypes.string,
- }).isRequired,
+ }),
...withLocalizePropTypes,
};
+const defaultProps = {
+ credentials: {
+ login: '',
+ },
+};
+
const ChangeExpensifyLoginLink = props => (
@@ -45,6 +51,7 @@ const ChangeExpensifyLoginLink = props => (
);
ChangeExpensifyLoginLink.propTypes = propTypes;
+ChangeExpensifyLoginLink.defaultProps = defaultProps;
ChangeExpensifyLoginLink.displayName = 'ChangeExpensifyLoginLink';
export default compose(
diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm.js
index c791b556575b..f70ccbb0a4de 100755
--- a/src/pages/signin/LoginForm.js
+++ b/src/pages/signin/LoginForm.js
@@ -17,6 +17,7 @@ import getEmailKeyboardType from '../../libs/getEmailKeyboardType';
import ExpensiTextInput from '../../components/ExpensiTextInput';
import * as ValidationUtils from '../../libs/ValidationUtils';
import LoginUtil from '../../libs/LoginUtil';
+import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../components/withToggleVisibilityView';
const propTypes = {
/* Onyx Props */
@@ -36,6 +37,8 @@ const propTypes = {
...windowDimensionsPropTypes,
...withLocalizePropTypes,
+
+ ...toggleVisibilityViewPropTypes,
};
const defaultProps = {
@@ -45,7 +48,6 @@ const defaultProps = {
class LoginForm extends React.Component {
constructor(props) {
super(props);
-
this.onTextInput = this.onTextInput.bind(this);
this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
@@ -55,6 +57,24 @@ class LoginForm extends React.Component {
};
}
+ componentDidMount() {
+ if (!canFocusInputOnScreenFocus() || !this.input) {
+ return;
+ }
+ this.input.focus();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.isVisible || !this.props.isVisible) {
+ return;
+ }
+ this.input.focus();
+
+ if (this.state.login) {
+ this.clearLogin();
+ }
+ }
+
/**
* Handle text input and clear formError upon text change
*
@@ -71,6 +91,13 @@ class LoginForm extends React.Component {
}
}
+ /**
+ * Clear Login from the state
+ */
+ clearLogin() {
+ this.setState({login: ''}, this.input.clear);
+ }
+
/**
* Check that all the form fields are valid, then trigger the submit callback
*/
@@ -105,16 +132,18 @@ class LoginForm extends React.Component {
<>
this.input = el}
label={this.props.translate('loginForm.phoneOrEmail')}
value={this.state.login}
- autoCompleteType="email"
+ autoCompleteType="username"
textContentType="username"
+ nativeID="username"
+ name="username"
onChangeText={this.onTextInput}
onSubmitEditing={this.validateAndSubmitForm}
autoCapitalize="none"
autoCorrect={false}
keyboardType={getEmailKeyboardType()}
- autoFocus={canFocusInputOnScreenFocus()}
/>
{this.state.formError && (
@@ -141,7 +170,6 @@ class LoginForm extends React.Component {
onPress={this.validateAndSubmitForm}
/>
-
>
);
}
@@ -156,4 +184,5 @@ export default compose(
}),
withWindowDimensions,
withLocalize,
+ withToggleVisibilityView,
)(LoginForm);
diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js
index 391130bf56b4..7b73a5937ee2 100755
--- a/src/pages/signin/PasswordForm.js
+++ b/src/pages/signin/PasswordForm.js
@@ -16,6 +16,8 @@ import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import compose from '../../libs/compose';
import ExpensiTextInput from '../../components/ExpensiTextInput';
+import * as ComponentUtils from '../../libs/ComponentUtils';
+import withToggleVisibilityView, {toggleVisibilityViewPropTypes} from '../../components/withToggleVisibilityView';
const propTypes = {
/* Onyx Props */
@@ -33,6 +35,7 @@ const propTypes = {
}),
...withLocalizePropTypes,
+ ...toggleVisibilityViewPropTypes,
};
const defaultProps = {
@@ -42,7 +45,6 @@ const defaultProps = {
class PasswordForm extends React.Component {
constructor(props) {
super(props);
-
this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
this.state = {
@@ -52,6 +54,20 @@ class PasswordForm extends React.Component {
};
}
+ componentDidMount() {
+ if (!this.input) {
+ return;
+ }
+ this.input.focus();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.isVisible || !this.props.isVisible) {
+ return;
+ }
+ this.input.focus();
+ }
+
/**
* Check that all the form fields are valid, then trigger the submit callback
*/
@@ -83,14 +99,16 @@ class PasswordForm extends React.Component {
<>
this.input = el}
label={this.props.translate('common.password')}
secureTextEntry
- autoCompleteType="password"
+ autoCompleteType={ComponentUtils.PASSWORD_AUTOCOMPLETE_TYPE}
textContentType="password"
+ nativeID="password"
+ name="password"
value={this.state.password}
onChangeText={text => this.setState({password: text})}
onSubmitEditing={this.validateAndSubmitForm}
- autoFocus
blurOnSubmit={false}
/>
@@ -155,4 +173,5 @@ export default compose(
withOnyx({
account: {key: ONYXKEYS.ACCOUNT},
}),
+ withToggleVisibilityView,
)(PasswordForm);
diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js
index 407f757c7aac..538abbf79b2c 100644
--- a/src/pages/signin/SignInPage.js
+++ b/src/pages/signin/SignInPage.js
@@ -81,21 +81,19 @@ class SignInPage extends Component {
const welcomeText = this.props.translate(`welcomeText.${showPasswordForm ? 'phrase4' : 'phrase1'}`);
return (
- <>
-
-
- {showLoginForm && }
-
- {showPasswordForm && }
-
- {showResendValidationLinkForm && }
-
-
- >
+
+
+ {/* LoginForm and PasswordForm must use the isVisible prop. This keeps them mounted, but visually hidden
+ so that password managers can access the values. Conditionally rendering these components will break this feature. */}
+
+
+ {showResendValidationLinkForm && }
+
+
);
}
}
diff --git a/src/pages/signin/SignInPageLayout/SignInPageLayoutNarrow.js b/src/pages/signin/SignInPageLayout/SignInPageLayoutNarrow.js
index 381f02273d39..dc2ad6f1aaa1 100755
--- a/src/pages/signin/SignInPageLayout/SignInPageLayoutNarrow.js
+++ b/src/pages/signin/SignInPageLayout/SignInPageLayoutNarrow.js
@@ -9,6 +9,7 @@ import ExpensifyCashLogo from '../../../components/ExpensifyCashLogo';
import Text from '../../../components/Text';
import TermsAndLicenses from '../TermsAndLicenses';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+import Form from '../../../components/Form';
import compose from '../../../libs/compose';
import scrollViewContentContainerStyles from './signInPageStyles.js';
import LoginKeyboardAvoidingView from './LoginKeyboardAvoidingView';
@@ -45,7 +46,7 @@ const SignInPageLayoutNarrow = props => (
]}
contentContainerStyle={scrollViewContentContainerStyles}
>
-
+
+
diff --git a/src/pages/signin/SignInPageLayout/SignInPageLayoutWide.js b/src/pages/signin/SignInPageLayout/SignInPageLayoutWide.js
index 065fb892a415..f2cf7680629a 100755
--- a/src/pages/signin/SignInPageLayout/SignInPageLayoutWide.js
+++ b/src/pages/signin/SignInPageLayout/SignInPageLayoutWide.js
@@ -9,6 +9,7 @@ import Text from '../../../components/Text';
import variables from '../../../styles/variables';
import TermsAndLicenses from '../TermsAndLicenses';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+import Form from '../../../components/Form';
const propTypes = {
/** The children to show inside the layout */
@@ -54,9 +55,7 @@ const SignInPageLayoutWide = props => (
{props.welcomeText}
)}
-
- {props.children}
-
+
diff --git a/src/styles/styles.js b/src/styles/styles.js
index bab7359d1532..0b618043c38b 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -427,6 +427,13 @@ const styles = {
width: variables.componentSizeNormal,
},
+ visuallyHidden: {
+ ...visibility('hidden'),
+ overflow: 'hidden',
+ width: 0,
+ height: 0,
+ },
+
loadingVBAAnimation: {
width: 160,
height: 160,