Merge pull request #2780 from jryans/auth-validation
Initial portions of support for Field validation
This commit is contained in:
commit
45063cad4a
18 changed files with 262 additions and 100 deletions
|
@ -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 = {
|
||||
|
@ -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";
|
||||
|
||||
|
@ -58,6 +77,8 @@ 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;
|
||||
|
@ -65,17 +86,37 @@ export default class Field extends React.PureComponent {
|
|||
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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
115
src/components/views/elements/Tooltip.js
Normal file
115
src/components/views/elements/Tooltip.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
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.
|
||||
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 React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'Tooltip',
|
||||
|
||||
propTypes: {
|
||||
// Class applied to the element used to position the tooltip
|
||||
className: React.PropTypes.string,
|
||||
// Class applied to the tooltip itself
|
||||
tooltipClassName: React.PropTypes.string,
|
||||
// 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_Tooltip_wrapper";
|
||||
document.body.appendChild(this.tooltipContainer);
|
||||
window.addEventListener('scroll', this._renderTooltip, true);
|
||||
|
||||
this.parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
|
||||
this._renderTooltip();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
this._renderTooltip();
|
||||
},
|
||||
|
||||
// Remove the wrapper element, as the tooltip has finished using it
|
||||
componentWillUnmount: function() {
|
||||
dis.dispatch({
|
||||
action: 'view_tooltip',
|
||||
tooltip: null,
|
||||
parent: null,
|
||||
});
|
||||
|
||||
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
|
||||
document.body.removeChild(this.tooltipContainer);
|
||||
window.removeEventListener('scroll', this._renderTooltip, true);
|
||||
},
|
||||
|
||||
_updatePosition(style) {
|
||||
const parentBox = this.parent.getBoundingClientRect();
|
||||
let offset = 0;
|
||||
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
|
||||
offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
|
||||
}
|
||||
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
|
||||
style.left = 6 + parentBox.right + window.pageXOffset;
|
||||
return style;
|
||||
},
|
||||
|
||||
_renderTooltip: function() {
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
// NOTE: The additional 6 pixels for the left position, is to take account of the
|
||||
// tooltips chevron
|
||||
const parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
let style = {};
|
||||
style = this._updatePosition(style);
|
||||
style.display = "block";
|
||||
|
||||
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName);
|
||||
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div className="mx_Tooltip_chevron" />
|
||||
{ this.props.label }
|
||||
</div>
|
||||
);
|
||||
|
||||
// Render the tooltip manually, as we wish it not to be rendered within the parent
|
||||
this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer);
|
||||
|
||||
// Tell the roomlist about us so it can manipulate us if it wishes
|
||||
dis.dispatch({
|
||||
action: 'view_tooltip',
|
||||
tooltip: this.tooltip,
|
||||
parent: parent,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Render a placeholder
|
||||
return (
|
||||
<div className={this.props.className} >
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue