mirror of https://github.com/vector-im/riot-web
adds validation for fields.
* renames RoomTooltip to be a generic Tooltip (which it is) * hooks it into Field to show validation results * adds onValidate to Field to let Field instances call an arbitrary validation function Rebased from @ara4n's https://github.com/matrix-org/matrix-react-sdk/pull/2550 by @jryans. Subsequent commits revise and adapt this work.pull/21833/head
parent
a5c1d6733f
commit
40f16fa310
|
@ -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";
|
||||
|
@ -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";
|
||||
|
|
|
@ -141,6 +141,39 @@ limitations under the License.
|
|||
color: $greyed-fg-color;
|
||||
}
|
||||
|
||||
.mx_Field_valid input,
|
||||
.mx_Field_valid select,
|
||||
.mx_Field_valid textarea {
|
||||
border-color: $input-valid-border-color ! important;
|
||||
}
|
||||
|
||||
.mx_Field_valid input + label,
|
||||
.mx_Field_valid select + label,
|
||||
.mx_Field_valid textarea + label {
|
||||
color: $input-valid-border-color ! important;
|
||||
}
|
||||
|
||||
.mx_Field_invalid input,
|
||||
.mx_Field_invalid select,
|
||||
.mx_Field_invalid textarea {
|
||||
border-color: $input-invalid-border-color ! important;
|
||||
}
|
||||
|
||||
.mx_Field_invalid input + label,
|
||||
.mx_Field_invalid select + label,
|
||||
.mx_Field_invalid textarea + label {
|
||||
color: $input-invalid-border-color ! important;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
|
@ -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.
|
||||
|
@ -14,41 +15,55 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_RoomTooltip_chevron {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 4px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 8px solid transparent;
|
||||
border-right: 8px solid $menu-bg-color;
|
||||
border-bottom: 8px solid transparent;
|
||||
@keyframes mx_fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.mx_RoomTooltip_chevron:after {
|
||||
content:'';
|
||||
@keyframes mx_fadeout {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.mx_Tooltip_chevron {
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
top: 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 7px solid transparent;
|
||||
border-right: 7px solid $primary-bg-color;
|
||||
border-right: 7px solid $menu-border-color;
|
||||
border-bottom: 7px solid transparent;
|
||||
position:absolute;
|
||||
top: -7px;
|
||||
}
|
||||
|
||||
.mx_Tooltip_chevron:after {
|
||||
content:'';
|
||||
width: 0;
|
||||
height: 0;
|
||||
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;
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -90,6 +90,15 @@ export default class ServerConfig extends React.PureComponent {
|
|||
this.setState({ hsUrl });
|
||||
}
|
||||
|
||||
onHomeserverValidate = (value) => {
|
||||
try {
|
||||
new URL(value);
|
||||
return { valid: true, feedback: <div>Valid URL!</div> };
|
||||
} catch (_) {
|
||||
return { valid: false, feedback: <div>Invalid URL!</div>};
|
||||
}
|
||||
}
|
||||
|
||||
onIdentityServerBlur = (ev) => {
|
||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
|
||||
this.props.onServerConfigChange({
|
||||
|
@ -134,6 +143,7 @@ export default class ServerConfig extends React.PureComponent {
|
|||
value={this.state.hsUrl}
|
||||
onBlur={this.onHomeserverBlur}
|
||||
onChange={this.onHomeserverChange}
|
||||
onValidate={this.onHomeserverValidate}
|
||||
/>
|
||||
<Field id="mx_ServerConfig_isUrl"
|
||||
label={_t("Identity Server URL")}
|
||||
|
|
|
@ -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 ?
|
||||
|
|
|
@ -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 = {
|
||||
|
@ -33,9 +34,22 @@ export default class Field extends React.PureComponent {
|
|||
placeholder: PropTypes.string,
|
||||
// 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.function,
|
||||
// All other props pass through to the <input>.
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
valid: undefined,
|
||||
feedback: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
get value() {
|
||||
if (!this.refs.fieldInput) return null;
|
||||
return this.refs.fieldInput.value;
|
||||
|
@ -48,8 +62,18 @@ export default class Field extends React.PureComponent {
|
|||
this.refs.fieldInput.value = newValue;
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
if (this.props.onValidate) {
|
||||
const result = this.props.onValidate(this.value);
|
||||
this.setState({
|
||||
valid: result.valid,
|
||||
feedback: result.feedback,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { element, prefix, children, ...inputProps } = this.props;
|
||||
const { element, prefix, onValidate, children, ...inputProps } = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
|
@ -58,6 +82,12 @@ export default class Field extends React.PureComponent {
|
|||
inputProps.ref = "fieldInput";
|
||||
inputProps.placeholder = inputProps.placeholder || inputProps.label;
|
||||
|
||||
inputProps.onChange = this.onChange;
|
||||
// make sure we use the current `value` for the field and not the original one
|
||||
if (this.value != undefined) {
|
||||
inputProps.value = this.value;
|
||||
}
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
|
||||
let prefixContainer = null;
|
||||
|
@ -65,17 +95,34 @@ export default class Field extends React.PureComponent {
|
|||
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_Field", `mx_Field_${inputElement}`, {
|
||||
const 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 ${validClass}`}
|
||||
label={this.state.feedback}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className={fieldClasses}>
|
||||
{prefixContainer}
|
||||
{fieldInput}
|
||||
<label htmlFor={this.props.id}>{this.props.label}</label>
|
||||
{feedback}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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.
|
||||
|
@ -23,22 +24,21 @@ import classNames from 'classnames';
|
|||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTooltip',
|
||||
displayName: 'Tooltip',
|
||||
|
||||
propTypes: {
|
||||
// Class applied to the element used to position the tooltip
|
||||
className: React.PropTypes.string.isRequired,
|
||||
className: React.PropTypes.string,
|
||||
// Class applied to the tooltip itself
|
||||
tooltipClassName: React.PropTypes.string,
|
||||
// The tooltip is derived from either the room name or a label
|
||||
room: React.PropTypes.object,
|
||||
// the react element to put into the tooltip
|
||||
label: React.PropTypes.node,
|
||||
},
|
||||
|
||||
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
||||
componentDidMount: function() {
|
||||
this.tooltipContainer = document.createElement("div");
|
||||
this.tooltipContainer.className = "mx_RoomTileTooltip_wrapper";
|
||||
this.tooltipContainer.className = "mx_Tooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this._renderTooltip, true);
|
||||
|
||||
|
@ -85,11 +85,11 @@ module.exports = React.createClass({
|
|||
style = this._updatePosition(style);
|
||||
style.display = "block";
|
||||
|
||||
const tooltipClasses = classNames("mx_RoomTooltip", this.props.tooltipClassName);
|
||||
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName);
|
||||
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div className="mx_RoomTooltip_chevron" />
|
||||
<div className="mx_Tooltip_chevron" />
|
||||
{ this.props.label }
|
||||
</div>
|
||||
);
|
|
@ -143,8 +143,8 @@ export default React.createClass({
|
|||
|
||||
let tooltip;
|
||||
if (this.props.collapsed && this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
|
||||
}
|
||||
|
||||
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
|
||||
|
|
|
@ -44,9 +44,9 @@ export default class MStickerBody extends MImageBody {
|
|||
|
||||
if (!content || !content.body || !content.info || !content.info.w) return null;
|
||||
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
const Tooltip = sdk.getComponent('elements.Tooltip');
|
||||
return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip">
|
||||
<RoomTooltip label={content.body} />
|
||||
<Tooltip label={content.body} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -364,8 +364,8 @@ module.exports = React.createClass({
|
|||
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
|
||||
}
|
||||
} else if (this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
}
|
||||
|
||||
//var incomingCallBox;
|
||||
|
|
Loading…
Reference in New Issue