Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Initial portions of support for Field validation #2780

Merged
merged 11 commits into from
Mar 14, 2019
2 changes: 1 addition & 1 deletion res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
@import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_ToggleSwitch.scss";
@import "./views/elements/_ToolTipButton.scss";
@import "./views/elements/_Tooltip.scss";
@import "./views/globals/_MatrixToolbar.scss";
@import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss";
Expand Down Expand Up @@ -137,7 +138,6 @@
@import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTooltip.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SearchableEntityList.scss";
Expand Down
33 changes: 33 additions & 0 deletions res/css/views/elements/_Field.scss
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,39 @@ limitations under the License.
color: $greyed-fg-color;
}

.mx_Field_valid {
&.mx_Field,
&.mx_Field:focus-within {
border-color: $input-valid-border-color;
}

&.mx_Field label,
&.mx_Field:focus-within label {
color: $input-valid-border-color;
}
}

.mx_Field_invalid {
&.mx_Field,
&.mx_Field:focus-within {
border-color: $input-invalid-border-color;
}

&.mx_Field label,
&.mx_Field:focus-within label {
color: $input-invalid-border-color;
}
}

.mx_Field_tooltip {
margin-top: -12px;
margin-left: 4px;
}

.mx_Field_tooltip.mx_Field_valid {
animation: mx_fadeout 1s 2s forwards;
}

// Customise other components when placed inside a Field

.mx_Field .mx_Dropdown_input {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
Expand All @@ -14,41 +15,55 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

.mx_RoomTooltip_chevron {
@keyframes mx_fadein {
from { opacity: 0; }
to { opacity: 1; }
}

@keyframes mx_fadeout {
from { opacity: 1; }
to { opacity: 0; }
}

.mx_Tooltip_chevron {
position: absolute;
left: -8px;
top: 4px;
left: -7px;
top: 10px;
width: 0;
height: 0;
border-top: 8px solid transparent;
border-right: 8px solid $menu-bg-color;
border-bottom: 8px solid transparent;
border-top: 7px solid transparent;
border-right: 7px solid $menu-border-color;
border-bottom: 7px solid transparent;
}

.mx_RoomTooltip_chevron:after {
.mx_Tooltip_chevron:after {
content:'';
width: 0;
height: 0;
border-top: 7px solid transparent;
border-right: 7px solid $primary-bg-color;
border-bottom: 7px solid transparent;
position:absolute;
top: -7px;
border-top: 6px solid transparent;
border-right: 6px solid $menu-bg-color;
border-bottom: 6px solid transparent;
position: absolute;
top: -6px;
left: 1px;
}

.mx_RoomTooltip {
.mx_Tooltip {
display: none;
animation: mx_fadein 0.2s;
position: fixed;
border-radius: 5px;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.21);
background-color: $primary-bg-color;
border: 1px solid $menu-border-color;
border-radius: 4px;
box-shadow: 4px 4px 12px 0 rgba(118, 131, 156, 0.6);
background-color: $menu-bg-color;
z-index: 2000;
padding: 5px;
padding: 10px;
pointer-events: none;
line-height: 14px;
font-size: 13px;
font-size: 12px;
font-weight: 600;
color: $primary-fg-color;
max-width: 600px;
max-width: 200px;
word-break: break-word;
margin-right: 50px;
}
1 change: 1 addition & 0 deletions res/themes/dark/css/_dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ $input-lighter-bg-color: #f2f5f8;
$input-lighter-fg-color: $input-darker-fg-color;
$input-focused-border-color: #238cf5;
$input-valid-border-color: $accent-color;
$input-invalid-border-color: $warning-color;

$field-focused-label-bg-color: $bg-color;

Expand Down
3 changes: 2 additions & 1 deletion res/themes/light/css/_light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ $input-lighter-bg-color: #f2f5f8;
$input-lighter-fg-color: $input-darker-fg-color;
$input-focused-border-color: #238cf5;
$input-valid-border-color: $accent-color;
$input-invalid-border-color: $warning-color;

$field-focused-label-bg-color: #ffffff;

Expand All @@ -95,7 +96,7 @@ $input-fg-color: rgba(74, 74, 74, 0.9);
$scrollbar-thumb-color: rgba(0, 0, 0, 0.2);
$scrollbar-track-color: transparent;
// context menus
$menu-border-color: #ebedf8;
$menu-border-color: #e7e7e7;
$menu-bg-color: #fff;
$menu-box-shadow-color: rgba(118, 131, 156, 0.6);
$menu-selected-color: #f5f8fa;
Expand Down
4 changes: 2 additions & 2 deletions src/components/structures/BottomLeftMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ module.exports = React.createClass({
// Get the label/tooltip to show
getLabel: function(label, show) {
if (show) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
return <RoomTooltip className="mx_BottomLeftMenu_tooltip" label={label} />;
const Tooltip = sdk.getComponent("elements.Tooltip");
return <Tooltip className="mx_BottomLeftMenu_tooltip" label={label} />;
}
},

Expand Down
9 changes: 0 additions & 9 deletions src/components/views/auth/PasswordLogin.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ class PasswordLogin extends React.Component {
this.isLoginEmpty = this.isLoginEmpty.bind(this);
}

componentWillMount() {
this._passwordField = null;
this._loginField = null;
}

onForgotPasswordClick(ev) {
ev.preventDefault();
ev.stopPropagation();
Expand Down Expand Up @@ -180,7 +175,6 @@ class PasswordLogin extends React.Component {
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"
Expand All @@ -196,7 +190,6 @@ class PasswordLogin extends React.Component {
return <Field
className={classNames(classes)}
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"
Expand All @@ -223,7 +216,6 @@ class PasswordLogin extends React.Component {
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_phoneNumber"
ref={(e) => { this._loginField = e; }}
name="phoneNumber"
key="phone_input"
type="text"
Expand Down Expand Up @@ -344,7 +336,6 @@ class PasswordLogin extends React.Component {
<Field
className={pwFieldClass}
id="mx_PasswordLogin_password"
ref={(e) => { this._passwordField = e; }}
type="password"
name="password"
label={_t('Password')}
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/elements/ActionButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export default React.createClass({

let tooltip;
if (this.state.showTooltip) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
}

const icon = this.props.iconPath ?
Expand Down
63 changes: 52 additions & 11 deletions src/components/views/elements/Field.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import sdk from '../../../index';

export default class Field extends React.PureComponent {
static propTypes = {
Expand All @@ -31,25 +32,43 @@ export default class Field extends React.PureComponent {
label: PropTypes.string,
// The field's placeholder string. Defaults to the label.
placeholder: PropTypes.string,
// The field's value.
// This is a controlled component, so the value is required.
value: PropTypes.string.isRequired,
// Optional component to include inside the field before the input.
prefix: PropTypes.node,
// The callback called whenever the contents of the field
// changes. Returns an object with `valid` boolean field
// and a `feedback` react component field to provide feedback
// to the user.
onValidate: PropTypes.func,
// All other props pass through to the <input>.
};

get value() {
if (!this.refs.fieldInput) return null;
return this.refs.fieldInput.value;
constructor() {
super();
this.state = {
valid: undefined,
feedback: undefined,
};
}

set value(newValue) {
if (!this.refs.fieldInput) {
throw new Error("No field input reference");
onChange = (ev) => {
if (this.props.onValidate) {
const result = this.props.onValidate(ev.target.value);
this.setState({
valid: result.valid,
feedback: result.feedback,
});
}
this.refs.fieldInput.value = newValue;
}
// Parent component may have supplied its own `onChange` as well
if (this.props.onChange) {
this.props.onChange(ev);
}
};

render() {
const { element, prefix, children, ...inputProps } = this.props;
const { element, prefix, onValidate, children, ...inputProps } = this.props;

const inputElement = element || "input";

Expand All @@ -58,24 +77,46 @@ export default class Field extends React.PureComponent {
inputProps.ref = "fieldInput";
inputProps.placeholder = inputProps.placeholder || inputProps.label;

inputProps.onChange = this.onChange;

const fieldInput = React.createElement(inputElement, inputProps, children);

let prefixContainer = null;
if (prefix) {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
}

const classes = classNames("mx_Field", `mx_Field_${inputElement}`, {
let validClass;
if (onValidate) {
validClass = classNames({
mx_Field_valid: this.state.valid === true,
mx_Field_invalid: this.state.valid === false,
});
}

const fieldClasses = 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,
[validClass]: true,
});

return <div className={classes}>
// handle displaying feedback on validity
const Tooltip = sdk.getComponent("elements.Tooltip");
let feedback;
if (this.state.feedback) {
feedback = <Tooltip
tooltipClassName="mx_Field_tooltip"
label={this.state.feedback}
/>;
}

return <div className={fieldClasses}>
{prefixContainer}
{fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label>
{feedback}
</div>;
}
}
4 changes: 2 additions & 2 deletions src/components/views/elements/TagTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export default React.createClass({
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
const Tooltip = sdk.getComponent('elements.Tooltip');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarHeight = 40;
Expand All @@ -181,7 +181,7 @@ export default React.createClass({
}

const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<Tooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
const contextButton = this.state.hover || this.state.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/elements/ToolTipButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ module.exports = React.createClass({
},

render: function() {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
const tip = this.state.hover ? <RoomTooltip
const Tooltip = sdk.getComponent("elements.Tooltip");
const tip = this.state.hover ? <Tooltip
className="mx_ToolTipButton_container"
tooltipClassName="mx_ToolTipButton_helpText"
label={this.props.helpText}
Expand Down
Loading