Merge pull request #2749 from jryans/auth-field

Use Field component in auth flows
This commit is contained in:
J. Ryan Stinnett 2019-03-06 11:18:24 +00:00 committed by GitHub
commit 8bf5e1d19f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 212 additions and 186 deletions

View file

@ -15,17 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_Login_field {
width: 100%;
box-sizing: border-box;
border-radius: 3px;
border: 1px solid $strong-input-border-color;
font-weight: 300;
font-size: 13px;
padding: 9px;
margin-bottom: 14px;
}
.mx_Login_submit {
@mixin mx_DialogButton;
width: 100%;
@ -69,74 +58,24 @@ limitations under the License.
color: $warning-color;
font-weight: bold;
text-align: center;
/*
height: 24px;
*/
margin-top: 12px;
margin-bottom: 12px;
}
.mx_Login_type_container {
display: flex;
margin-bottom: 14px;
align-items: center;
color: $authpage-primary-color;
.mx_Field {
margin: 0;
}
}
.mx_Login_type_label {
flex-grow: 1;
line-height: 35px;
}
.mx_Login_type_dropdown {
display: inline-block;
min-width: 170px;
align-self: flex-end;
flex: 1 1 auto;
}
.mx_Login_field_prefix {
height: 38px;
padding: 0px 5px;
line-height: 38px;
background-color: #eee;
border: 1px solid #c7c7c7;
border-right: 0px;
border-radius: 3px 0px 0px 3px;
text-align: center;
}
.mx_Login_field_has_prefix {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.mx_Login_phoneSection {
display:flex;
}
.mx_Login_phoneCountry {
margin-bottom: 14px;
/* To override mx_Login_field_prefix */
text-align: left;
padding: 0px;
background-color: $primary-bg-color;
}
.mx_Login_field_prefix .mx_Dropdown_input {
/* To use prefix border instead of dropdown border */
border: 0;
}
.mx_Login_phoneCountry .mx_Dropdown_option {
/* To match height of mx_Login_field */
height: 38px;
line-height: 38px;
}
.mx_Login_phoneCountry .mx_Dropdown_option img {
margin: 3px;
vertical-align: top;
min-width: 200px;
}

View file

@ -78,16 +78,16 @@ limitations under the License.
margin-bottom: 10px;
}
.mx_AuthBody_fieldRow > * {
.mx_AuthBody_fieldRow > .mx_Field {
margin: 0 5px;
flex: 1;
}
.mx_AuthBody_fieldRow > *:first-child {
.mx_AuthBody_fieldRow > .mx_Field:first-child {
margin-left: 0;
}
.mx_AuthBody_fieldRow > *:last-child {
.mx_AuthBody_fieldRow > .mx_Field:last-child {
margin-right: 0;
}

View file

@ -17,8 +17,16 @@ limitations under the License.
/* TODO: Consider unifying with general input styles in _light.scss */
.mx_Field {
display: flex;
position: relative;
margin: 1em 0;
border-radius: 4px;
transition: border-color 0.25s;
border: 1px solid $input-border-color;
}
.mx_Field_prefix {
border-right: 1px solid $input-border-color;
}
.mx_Field input,
@ -27,9 +35,10 @@ limitations under the License.
font-weight: normal;
font-family: $font-family;
font-size: 14px;
border: none;
// Even without a border here, we still need this avoid overlapping the rounded
// corners on the field above.
border-radius: 4px;
transition: border-color 0.25s;
border: 1px solid $input-border-color;
padding: 8px 9px;
color: $primary-fg-color;
background-color: $primary-bg-color;
@ -55,11 +64,14 @@ limitations under the License.
pointer-events: none;
}
.mx_Field:focus-within {
border-color: $input-focused-border-color;
}
.mx_Field input:focus,
.mx_Field select:focus,
.mx_Field textarea:focus {
outline: 0;
border-color: $input-focused-border-color;
}
.mx_Field input::placeholder,
@ -99,7 +111,8 @@ limitations under the License.
.mx_Field input:not(:placeholder-shown) + label,
.mx_Field textarea:focus + label,
.mx_Field textarea:not(:placeholder-shown) + label,
.mx_Field select + label /* Always show a select's label on top to not collide with the value */ {
.mx_Field select + label /* Always show a select's label on top to not collide with the value */,
.mx_Field_labelAlwaysTopLeft label {
transition:
font-size 0.25s ease-out 0s,
color 0.25s ease-out 0s,
@ -127,3 +140,14 @@ limitations under the License.
background-color: $field-focused-label-bg-color;
color: $greyed-fg-color;
}
// Customise other components when placed inside a Field
.mx_Field .mx_Dropdown_input {
border: initial;
border-radius: initial;
}
.mx_Field .mx_CountryDropdown {
width: 67px;
}

View file

@ -230,6 +230,8 @@ module.exports = React.createClass({
},
renderForgot() {
const Field = sdk.getComponent('elements.Field');
let errorText = null;
const err = this.state.errorText || this.props.defaultServerDiscoveryError;
if (err) {
@ -275,23 +277,33 @@ module.exports = React.createClass({
{errorText}
<form onSubmit={this.onSubmitForm}>
<div className="mx_AuthBody_fieldRow">
<input className="mx_Login_field" type="text"
<Field
id="mx_ForgotPassword_email"
name="reset_email" // define a name so browser's password autofill gets less confused
type="text"
label={_t('Email')}
value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")}
placeholder={_t('Email')} autoFocus />
autoFocus
/>
</div>
<div className="mx_AuthBody_fieldRow">
<input className="mx_Login_field" type="password"
<Field
id="mx_ForgotPassword_password"
name="reset_password"
type="password"
label={_t('Password')}
value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")}
placeholder={_t('Password')} />
<input className="mx_Login_field" type="password"
/>
<Field
id="mx_ForgotPassword_passwordConfirm"
name="reset_password_confirm"
type="password"
label={_t('Confirm')}
value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")}
placeholder={_t('Confirm')} />
/>
</div>
<span>{_t(
'A verification email will be sent to your inbox to confirm ' +

View file

@ -138,7 +138,8 @@ class PasswordLogin extends React.Component {
this.props.onUsernameBlur(ev.target.value);
}
onLoginTypeChange(loginType) {
onLoginTypeChange(ev) {
const loginType = ev.target.value;
this.props.onError(null); // send a null error to clear any error messages
this.setState({
loginType: loginType,
@ -169,67 +170,70 @@ class PasswordLogin extends React.Component {
}
renderLoginField(loginType) {
const classes = {
mx_Login_field: true,
};
const Field = sdk.getComponent('elements.Field');
const classes = {};
switch (loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
classes.error = this.props.loginIncorrect && !this.state.username;
return <input
className="mx_Login_field"
ref={(e) => {this._loginField = e;}}
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_email"
ref={(e) => { this._loginField = e; }}
name="username" // make it a little easier for browser's remember-password
key="email_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
label={_t("Email")}
placeholder="joe@example.com"
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
autoFocus
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
classes.error = this.props.loginIncorrect && !this.state.username;
return <input
return <Field
className={classNames(classes)}
ref={(e) => {this._loginField = e;}}
id="mx_PasswordLogin_username"
ref={(e) => { this._loginField = e; }}
name="username" // make it a little easier for browser's remember-password
key="username_input"
type="text"
name="username" // make it a little easier for browser's remember-password
label={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("Username")}
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
placeholder={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
}) : _t("Username")}
value={this.state.username}
autoFocus
/>;
case PasswordLogin.LOGIN_FIELD_PHONE: {
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
classes.mx_Login_field_has_prefix = true;
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
return <div className="mx_Login_phoneSection">
<CountryDropdown
className="mx_Login_phoneCountry mx_Login_field_prefix"
onOptionChange={this.onPhoneCountryChanged}
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
/>
<input
className={classNames(classes)}
ref={(e) => {this._loginField = e;}}
key="phone_input"
type="text"
name="phoneNumber"
onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur}
placeholder={_t("Mobile phone number")}
value={this.state.phoneNumber}
autoFocus
/>
</div>;
const phoneCountry = <CountryDropdown
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
onOptionChange={this.onPhoneCountryChanged}
/>;
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_phoneNumber"
ref={(e) => { this._loginField = e; }}
name="phoneNumber"
key="phone_input"
type="text"
label={_t("Phone")}
value={this.state.phoneNumber}
prefix={phoneCountry}
onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur}
autoFocus
/>;
}
}
}
@ -245,6 +249,8 @@ class PasswordLogin extends React.Component {
}
render() {
const Field = sdk.getComponent('elements.Field');
let forgotPasswordJsx;
if (this.props.onForgotPasswordClick) {
@ -286,12 +292,9 @@ class PasswordLogin extends React.Component {
}
const pwFieldClass = classNames({
mx_Login_field: true,
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
});
const Dropdown = sdk.getComponent('elements.Dropdown');
const loginField = this.renderLoginField(this.state.loginType);
let loginType;
@ -299,14 +302,32 @@ class PasswordLogin extends React.Component {
loginType = (
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Dropdown
<Field
className="mx_Login_type_dropdown"
id="mx_PasswordLogin_type"
element="select"
value={this.state.loginType}
onOptionChange={this.onLoginTypeChange}>
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('Username') }</span>
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown>
onChange={this.onLoginTypeChange}
>
<option
key={PasswordLogin.LOGIN_FIELD_MXID}
value={PasswordLogin.LOGIN_FIELD_MXID}
>
{_t('Username')}
</option>
<option
key={PasswordLogin.LOGIN_FIELD_EMAIL}
value={PasswordLogin.LOGIN_FIELD_EMAIL}
>
{_t('Email address')}
</option>
<option
key={PasswordLogin.LOGIN_FIELD_PHONE}
value={PasswordLogin.LOGIN_FIELD_PHONE}
>
{_t('Phone')}
</option>
</Field>
</div>
);
}
@ -318,15 +339,19 @@ class PasswordLogin extends React.Component {
{editLink}
</h3>
<form onSubmit={this.onSubmitForm}>
{ loginType }
{ loginField }
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
{loginType}
{loginField}
<Field
className={pwFieldClass}
id="mx_PasswordLogin_password"
ref={(e) => { this._passwordField = e; }}
type="password"
name="password"
value={this.state.password} onChange={this.onPasswordChanged}
placeholder={_t('Password')}
label={_t('Password')}
value={this.state.password}
onChange={this.onPasswordChanged}
/>
<br />
{ forgotPasswordJsx }
{forgotPasswordJsx}
<input className="mx_Login_submit"
type="submit"
value={_t('Sign in')}

View file

@ -306,6 +306,8 @@ module.exports = React.createClass({
},
render: function() {
const Field = sdk.getComponent('elements.Field');
let yourMatrixAccountText = _t('Create your Matrix account');
if (this.props.hsName) {
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
@ -338,14 +340,16 @@ module.exports = React.createClass({
_t("Email (optional)");
emailSection = (
<div>
<input type="text" ref="email"
placeholder={emailPlaceholder}
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={this.onEmailBlur}
value={this.state.email} />
</div>
<Field
className={this._classForField(FIELD_EMAIL)}
id="mx_RegistrationForm_email"
ref="email"
type="text"
label={emailPlaceholder}
defaultValue={this.props.defaultEmail}
value={this.state.email}
onBlur={this.onEmailBlur}
/>
);
}
@ -353,40 +357,33 @@ module.exports = React.createClass({
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
let phoneSection;
if (threePidLogin && this._authStepIsUsed('m.login.msisdn')) {
const phonePlaceholder = this._authStepIsRequired('m.login.msisdn') ?
const phoneLabel = this._authStepIsRequired('m.login.msisdn') ?
_t("Phone") :
_t("Phone (optional)");
phoneSection = (
<div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country"
className="mx_Login_phoneCountry mx_Login_field_prefix"
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
onOptionChange={this.onPhoneCountryChange}
/>
<input type="text" ref="phoneNumber"
placeholder={phonePlaceholder}
defaultValue={this.props.defaultPhoneNumber}
className={this._classForField(
FIELD_PHONE_NUMBER,
'mx_Login_phoneNumberField',
'mx_Login_field',
'mx_Login_field_has_prefix',
)}
onBlur={this.onPhoneNumberBlur}
value={this.state.phoneNumber}
/>
</div>
);
const phoneCountry = <CountryDropdown
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
onOptionChange={this.onPhoneCountryChange}
/>;
phoneSection = <Field
className={this._classForField(FIELD_PHONE_NUMBER)}
id="mx_RegistrationForm_phoneNumber"
ref="phoneNumber"
type="text"
label={phoneLabel}
defaultValue={this.props.defaultPhoneNumber}
value={this.state.phoneNumber}
prefix={phoneCountry}
onBlur={this.onPhoneNumberBlur}
/>;
}
const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
);
const placeholderUsername = _t("Username");
return (
<div>
<h3>
@ -395,22 +392,36 @@ module.exports = React.createClass({
</h3>
<form onSubmit={this.onSubmit}>
<div className="mx_AuthBody_fieldRow">
<input type="text" ref="username"
<Field
className={this._classForField(FIELD_USERNAME)}
id="mx_RegistrationForm_username"
ref="username"
type="text"
autoFocus={true}
placeholder={placeholderUsername} defaultValue={this.props.defaultUsername}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
onBlur={this.onUsernameBlur} />
label={_t("Username")}
defaultValue={this.props.defaultUsername}
onBlur={this.onUsernameBlur}
/>
</div>
<div className="mx_AuthBody_fieldRow">
<input type="password" ref="password"
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
<Field
className={this._classForField(FIELD_PASSWORD)}
id="mx_RegistrationForm_password"
ref="password"
type="password"
label={_t("Password")}
defaultValue={this.props.defaultPassword}
onBlur={this.onPasswordBlur}
placeholder={_t("Password")} defaultValue={this.props.defaultPassword} />
<input type="password" ref="passwordConfirm"
placeholder={_t("Confirm")}
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
/>
<Field
className={this._classForField(FIELD_PASSWORD_CONFIRM)}
id="mx_RegistrationForm_passwordConfirm"
ref="passwordConfirm"
type="password"
label={_t("Confirm")}
defaultValue={this.props.defaultPassword}
onBlur={this.onPasswordConfirmBlur}
defaultValue={this.props.defaultPassword} />
/>
</div>
<div className="mx_AuthBody_fieldRow">
{ emailSection }

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class Field extends React.PureComponent {
static propTypes = {
@ -30,6 +31,8 @@ export default class Field extends React.PureComponent {
label: PropTypes.string,
// The field's placeholder string. Defaults to the label.
placeholder: PropTypes.string,
// Optional component to include inside the field before the input.
prefix: PropTypes.node,
// All other props pass through to the <input>.
};
@ -46,7 +49,7 @@ export default class Field extends React.PureComponent {
}
render() {
const { element, children, ...inputProps } = this.props;
const { element, prefix, children, ...inputProps } = this.props;
const inputElement = element || "input";
@ -57,7 +60,20 @@ export default class Field extends React.PureComponent {
const fieldInput = React.createElement(inputElement, inputProps, children);
return <div className={`mx_Field mx_Field_${inputElement}`}>
let prefixContainer = null;
if (prefix) {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
}
const classes = classNames("mx_Field", `mx_Field_${inputElement}`, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefix,
});
return <div className={classes}>
{prefixContainer}
{fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label>
</div>;

View file

@ -1258,19 +1258,18 @@
"The username field must not be blank.": "The username field must not be blank.",
"The phone number field must not be blank.": "The phone number field must not be blank.",
"The password field must not be blank.": "The password field must not be blank.",
"Email": "Email",
"Username on %(hs)s": "Username on %(hs)s",
"Username": "Username",
"Mobile phone number": "Mobile phone number",
"Phone": "Phone",
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
"Sign in to your Matrix account": "Sign in to your Matrix account",
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
"Change": "Change",
"Sign in with": "Sign in with",
"Phone": "Phone",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Create your Matrix account": "Create your Matrix account",
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
"Email": "Email",
"Email (optional)": "Email (optional)",
"Phone (optional)": "Phone (optional)",
"Confirm": "Confirm",