diff --git a/res/css/_components.scss b/res/css/_components.scss index 1f896d270d..8bea138acb 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -100,6 +100,7 @@ @import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_Tooltip.scss"; +@import "./views/elements/_Validation.scss"; @import "./views/globals/_MatrixToolbar.scss"; @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; diff --git a/res/css/views/elements/_Validation.scss b/res/css/views/elements/_Validation.scss new file mode 100644 index 0000000000..08ae793663 --- /dev/null +++ b/res/css/views/elements/_Validation.scss @@ -0,0 +1,32 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_Validation_details { + padding-left: 15px; +} + +.mx_Validation_detail { + font-weight: normal; + + // TODO: Check / cross images + &.mx_Validation_valid { + color: $input-valid-border-color; + } + + &.mx_Validation_invalid { + color: $input-invalid-border-color; + } +} diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 1784ab61c3..83ea4dfae6 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -25,6 +25,7 @@ import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; +import withValidation from '../elements/Validation'; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_NUMBER = 'field_phone_number'; @@ -86,6 +87,7 @@ module.exports = React.createClass({ // is the one from the first invalid field. // It's not super ideal that this just calls // onValidationChange once for each invalid field. + // TODO: Change this to trigger new-style validation for an invalid fields. this.validateField(FIELD_PHONE_NUMBER, ev.type); this.validateField(FIELD_EMAIL, ev.type); this.validateField(FIELD_PASSWORD_CONFIRM, ev.type); @@ -152,6 +154,8 @@ module.exports = React.createClass({ const pwd2 = this.state.passwordConfirm.trim(); const allowEmpty = eventType === "blur"; + // TODO: Remove rules here as they are converted to new-style validation + switch (fieldID) { case FIELD_EMAIL: { const email = this.state.email; @@ -173,12 +177,6 @@ module.exports = React.createClass({ const username = this.state.username; if (allowEmpty && username === '') { this.markFieldValid(fieldID, true); - } else if (!SAFE_LOCALPART_REGEX.test(username)) { - this.markFieldValid( - fieldID, - false, - "RegistrationForm.ERR_USERNAME_INVALID", - ); } else if (username == '') { this.markFieldValid( fieldID, @@ -232,11 +230,14 @@ module.exports = React.createClass({ this.setState({ fieldErrors, }); + // TODO: Remove outer validation handling once all fields converted to new-style + // validation in the form. this.props.onValidationChange(fieldErrors); }, _classForField: function(fieldID, ...baseClasses) { let cls = baseClasses.join(' '); + // TODO: Remove this from fields as they are converted to new-style validation. if (this.state.fieldErrors[fieldID]) { if (cls) cls += ' '; cls += 'error'; @@ -291,10 +292,6 @@ module.exports = React.createClass({ }); }, - onUsernameBlur(ev) { - this.validateField(FIELD_USERNAME, ev.type); - }, - onUsernameChange(ev) { this.setState({ username: ev.target.value, @@ -325,6 +322,33 @@ module.exports = React.createClass({ }); }, + renderUsername() { + const Field = sdk.getComponent('elements.Field'); + + const onValidate = withValidation({ + description: _t("Use letters, numbers, dashes and underscores only"), + rules: [ + { + key: "safeLocalpart", + regex: SAFE_LOCALPART_REGEX, + invalid: _t("Some characters not allowed"), + }, + ], + }); + + return ; + }, + render: function() { const Field = sdk.getComponent('elements.Field'); @@ -412,17 +436,7 @@ module.exports = React.createClass({
- + {this.renderUsername()}
; @@ -108,7 +108,7 @@ export default class Field extends React.PureComponent { {prefixContainer} {fieldInput} - {feedback} + {tooltip}
; } } diff --git a/src/components/views/elements/Validation.js b/src/components/views/elements/Validation.js new file mode 100644 index 0000000000..21538609c1 --- /dev/null +++ b/src/components/views/elements/Validation.js @@ -0,0 +1,102 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import classNames from 'classnames'; + +/** + * Creates a validation function from a set of rules describing what to validate. + * + * @param {String} description + * Summary of the kind of value that will meet the validation rules. Shown at + * the top of the validation feedback. + * @param {Object} rules + * An array of rules describing how to check to input value. Each rule in an object + * and may have the following properties: + * - `key`: A unique ID for the rule. Required. + * - `regex`: A regex used to determine the rule's current validity. Required. + * - `valid`: Text to show when the rule is valid. Only shown if set. + * - `invalid`: Text to show when the rule is invalid. Only shown if set. + * @returns {Function} + * A validation function that takes in the current input value and returns + * the overall validity and a feedback UI that can be rendered for more detail. + */ +export default function withValidation({ description, rules }) { + return function onValidate(value) { + // TODO: Hide on blur + // TODO: Re-run only after ~200ms of inactivity + if (!value) { + return { + valid: null, + feedback: null, + }; + } + + const results = []; + let valid = true; + if (rules && rules.length) { + for (const rule of rules) { + if (!rule.key || !rule.regex) { + continue; + } + const ruleValid = rule.regex.test(value); + valid = valid && ruleValid; + if (ruleValid && rule.valid) { + // If the rule's result is valid and has text to show for + // the valid state, show it. + results.push({ + key: rule.key, + valid: true, + text: rule.valid, + }); + } else if (!ruleValid && rule.invalid) { + // If the rule's result is invalid and has text to show for + // the invalid state, show it. + results.push({ + key: rule.key, + valid: false, + text: rule.invalid, + }); + } + } + } + + let details; + if (results && results.length) { + details = ; + } + + const feedback =
+
{description}
+ {details} +
; + + return { + valid, + feedback, + }; + }; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a7fae2803f..51967c39f1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1322,6 +1322,8 @@ "Change": "Change", "Sign in with": "Sign in with", "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?", + "Use letters, numbers, dashes and underscores only": "Use letters, numbers, dashes and underscores only", + "Some characters not allowed": "Some characters not allowed", "Create your Matrix account": "Create your Matrix account", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Email (optional)": "Email (optional)",