Add validation feedback helper
This adds a general validation feedback mechanism for checking input values. An initial example is wired up for the username input on registration.pull/21833/head
parent
37ecf2a623
commit
338d83ab55
|
@ -100,6 +100,7 @@
|
||||||
@import "./views/elements/_ToggleSwitch.scss";
|
@import "./views/elements/_ToggleSwitch.scss";
|
||||||
@import "./views/elements/_ToolTipButton.scss";
|
@import "./views/elements/_ToolTipButton.scss";
|
||||||
@import "./views/elements/_Tooltip.scss";
|
@import "./views/elements/_Tooltip.scss";
|
||||||
|
@import "./views/elements/_Validation.scss";
|
||||||
@import "./views/globals/_MatrixToolbar.scss";
|
@import "./views/globals/_MatrixToolbar.scss";
|
||||||
@import "./views/groups/_GroupPublicityToggle.scss";
|
@import "./views/groups/_GroupPublicityToggle.scss";
|
||||||
@import "./views/groups/_GroupRoomList.scss";
|
@import "./views/groups/_GroupRoomList.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||||
|
import withValidation from '../elements/Validation';
|
||||||
|
|
||||||
const FIELD_EMAIL = 'field_email';
|
const FIELD_EMAIL = 'field_email';
|
||||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||||
|
@ -86,6 +87,7 @@ module.exports = React.createClass({
|
||||||
// is the one from the first invalid field.
|
// is the one from the first invalid field.
|
||||||
// It's not super ideal that this just calls
|
// It's not super ideal that this just calls
|
||||||
// onValidationChange once for each invalid field.
|
// 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_PHONE_NUMBER, ev.type);
|
||||||
this.validateField(FIELD_EMAIL, ev.type);
|
this.validateField(FIELD_EMAIL, ev.type);
|
||||||
this.validateField(FIELD_PASSWORD_CONFIRM, ev.type);
|
this.validateField(FIELD_PASSWORD_CONFIRM, ev.type);
|
||||||
|
@ -152,6 +154,8 @@ module.exports = React.createClass({
|
||||||
const pwd2 = this.state.passwordConfirm.trim();
|
const pwd2 = this.state.passwordConfirm.trim();
|
||||||
const allowEmpty = eventType === "blur";
|
const allowEmpty = eventType === "blur";
|
||||||
|
|
||||||
|
// TODO: Remove rules here as they are converted to new-style validation
|
||||||
|
|
||||||
switch (fieldID) {
|
switch (fieldID) {
|
||||||
case FIELD_EMAIL: {
|
case FIELD_EMAIL: {
|
||||||
const email = this.state.email;
|
const email = this.state.email;
|
||||||
|
@ -173,12 +177,6 @@ module.exports = React.createClass({
|
||||||
const username = this.state.username;
|
const username = this.state.username;
|
||||||
if (allowEmpty && username === '') {
|
if (allowEmpty && username === '') {
|
||||||
this.markFieldValid(fieldID, true);
|
this.markFieldValid(fieldID, true);
|
||||||
} else if (!SAFE_LOCALPART_REGEX.test(username)) {
|
|
||||||
this.markFieldValid(
|
|
||||||
fieldID,
|
|
||||||
false,
|
|
||||||
"RegistrationForm.ERR_USERNAME_INVALID",
|
|
||||||
);
|
|
||||||
} else if (username == '') {
|
} else if (username == '') {
|
||||||
this.markFieldValid(
|
this.markFieldValid(
|
||||||
fieldID,
|
fieldID,
|
||||||
|
@ -232,11 +230,14 @@ module.exports = React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
fieldErrors,
|
fieldErrors,
|
||||||
});
|
});
|
||||||
|
// TODO: Remove outer validation handling once all fields converted to new-style
|
||||||
|
// validation in the form.
|
||||||
this.props.onValidationChange(fieldErrors);
|
this.props.onValidationChange(fieldErrors);
|
||||||
},
|
},
|
||||||
|
|
||||||
_classForField: function(fieldID, ...baseClasses) {
|
_classForField: function(fieldID, ...baseClasses) {
|
||||||
let cls = baseClasses.join(' ');
|
let cls = baseClasses.join(' ');
|
||||||
|
// TODO: Remove this from fields as they are converted to new-style validation.
|
||||||
if (this.state.fieldErrors[fieldID]) {
|
if (this.state.fieldErrors[fieldID]) {
|
||||||
if (cls) cls += ' ';
|
if (cls) cls += ' ';
|
||||||
cls += 'error';
|
cls += 'error';
|
||||||
|
@ -291,10 +292,6 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onUsernameBlur(ev) {
|
|
||||||
this.validateField(FIELD_USERNAME, ev.type);
|
|
||||||
},
|
|
||||||
|
|
||||||
onUsernameChange(ev) {
|
onUsernameChange(ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
username: ev.target.value,
|
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 <Field
|
||||||
|
className={this._classForField(FIELD_USERNAME)}
|
||||||
|
id="mx_RegistrationForm_username"
|
||||||
|
type="text"
|
||||||
|
autoFocus={true}
|
||||||
|
label={_t("Username")}
|
||||||
|
defaultValue={this.props.defaultUsername}
|
||||||
|
value={this.state.username}
|
||||||
|
onChange={this.onUsernameChange}
|
||||||
|
onValidate={onValidate}
|
||||||
|
/>;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const Field = sdk.getComponent('elements.Field');
|
const Field = sdk.getComponent('elements.Field');
|
||||||
|
|
||||||
|
@ -412,17 +436,7 @@ module.exports = React.createClass({
|
||||||
</h3>
|
</h3>
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
<div className="mx_AuthBody_fieldRow">
|
<div className="mx_AuthBody_fieldRow">
|
||||||
<Field
|
{this.renderUsername()}
|
||||||
className={this._classForField(FIELD_USERNAME)}
|
|
||||||
id="mx_RegistrationForm_username"
|
|
||||||
type="text"
|
|
||||||
autoFocus={true}
|
|
||||||
label={_t("Username")}
|
|
||||||
defaultValue={this.props.defaultUsername}
|
|
||||||
value={this.state.username}
|
|
||||||
onBlur={this.onUsernameBlur}
|
|
||||||
onChange={this.onUsernameChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_AuthBody_fieldRow">
|
<div className="mx_AuthBody_fieldRow">
|
||||||
<Field
|
<Field
|
||||||
|
|
|
@ -94,11 +94,11 @@ export default class Field extends React.PureComponent {
|
||||||
mx_Field_invalid: onValidate && this.state.valid === false,
|
mx_Field_invalid: onValidate && this.state.valid === false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle displaying feedback on validity
|
// Handle displaying feedback on validity
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||||
let feedback;
|
let tooltip;
|
||||||
if (this.state.feedback) {
|
if (this.state.feedback) {
|
||||||
feedback = <Tooltip
|
tooltip = <Tooltip
|
||||||
tooltipClassName="mx_Field_tooltip"
|
tooltipClassName="mx_Field_tooltip"
|
||||||
label={this.state.feedback}
|
label={this.state.feedback}
|
||||||
/>;
|
/>;
|
||||||
|
@ -108,7 +108,7 @@ export default class Field extends React.PureComponent {
|
||||||
{prefixContainer}
|
{prefixContainer}
|
||||||
{fieldInput}
|
{fieldInput}
|
||||||
<label htmlFor={this.props.id}>{this.props.label}</label>
|
<label htmlFor={this.props.id}>{this.props.label}</label>
|
||||||
{feedback}
|
{tooltip}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = <ul className="mx_Validation_details">
|
||||||
|
{results.map(result => {
|
||||||
|
const classes = classNames({
|
||||||
|
"mx_Validation_detail": true,
|
||||||
|
"mx_Validation_valid": result.valid,
|
||||||
|
"mx_Validation_invalid": !result.valid,
|
||||||
|
});
|
||||||
|
return <li key={result.key} className={classes}>
|
||||||
|
{result.text}
|
||||||
|
</li>;
|
||||||
|
})}
|
||||||
|
</ul>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback = <div className="mx_Validation">
|
||||||
|
<div>{description}</div>
|
||||||
|
{details}
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid,
|
||||||
|
feedback,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -1322,6 +1322,8 @@
|
||||||
"Change": "Change",
|
"Change": "Change",
|
||||||
"Sign in with": "Sign in with",
|
"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?",
|
"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": "Create your Matrix account",
|
||||||
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
|
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
|
||||||
"Email (optional)": "Email (optional)",
|
"Email (optional)": "Email (optional)",
|
||||||
|
|
Loading…
Reference in New Issue