diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index d4b5e7402c7..2cf62765570 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -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; } diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index e8f49cdbd25..e7a6e04e8b6 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -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; } diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 02f0e548fb0..4a74262fd4f 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -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; +} diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 58deb380e38..f0a53e5063c 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -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}
- + autoFocus + />
- - + + />
{_t( 'A verification email will be sent to your inbox to confirm ' + diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 1ad93f60751..4b095d405f7 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -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 {this._loginField = e;}} + return { 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 {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
- - {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 - /> -
; + + const phoneCountry = ; + + return { 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 = (
- - { _t('Username') } - { _t('Email address') } - { _t('Phone') } - + onChange={this.onLoginTypeChange} + > + + + +
); } @@ -318,15 +339,19 @@ class PasswordLogin extends React.Component { {editLink} - { loginType } - { loginField } - {this._passwordField = e;}} type="password" + {loginType} + {loginField} + { 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} /> -
- { forgotPasswordJsx } + {forgotPasswordJsx} - - + ); } @@ -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 = ( -
- - -
- ); + const phoneCountry = ; + + phoneSection = ; } const registerButton = ( ); - const placeholderUsername = _t("Username"); - return (

@@ -395,22 +392,36 @@ module.exports = React.createClass({

- + label={_t("Username")} + defaultValue={this.props.defaultUsername} + onBlur={this.onUsernameBlur} + />
- - + + />
{ emailSection } diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 1b7d9fdd73a..c6a2113adb0 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -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 . }; @@ -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
+ let prefixContainer = null; + if (prefix) { + prefixContainer = {prefix}; + } + + 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
+ {prefixContainer} {fieldInput}
; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a1448aa6e6a..a17576826cc 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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? Set a new one": "Not sure of your password? Set a new one", "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",