diff --git a/res/css/_components.scss b/res/css/_components.scss
index 6f66a8c15e..4fb0eed4af 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -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";
diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss
index 4a74262fd4..22bc6a1800 100644
--- a/res/css/views/elements/_Field.scss
+++ b/res/css/views/elements/_Field.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 {
diff --git a/res/css/views/rooms/_RoomTooltip.scss b/res/css/views/elements/_Tooltip.scss
similarity index 54%
rename from res/css/views/rooms/_RoomTooltip.scss
rename to res/css/views/elements/_Tooltip.scss
index 295786d2d3..78604b1564 100644
--- a/res/css/views/rooms/_RoomTooltip.scss
+++ b/res/css/views/elements/_Tooltip.scss
@@ -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;
}
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index deed7235cb..3112644a73 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -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;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 4d8e4fa40e..879be67dda 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -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;
diff --git a/src/components/structures/BottomLeftMenu.js b/src/components/structures/BottomLeftMenu.js
index 47b30be450..2f48bd0299 100644
--- a/src/components/structures/BottomLeftMenu.js
+++ b/src/components/structures/BottomLeftMenu.js
@@ -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 ;
+ const Tooltip = sdk.getComponent("elements.Tooltip");
+ return ;
}
},
diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js
index cb0e0dc38e..ed6b4bdf7d 100644
--- a/src/components/views/auth/ServerConfig.js
+++ b/src/components/views/auth/ServerConfig.js
@@ -90,6 +90,15 @@ export default class ServerConfig extends React.PureComponent {
this.setState({ hsUrl });
}
+ onHomeserverValidate = (value) => {
+ try {
+ new URL(value);
+ return { valid: true, feedback:
Valid URL!
};
+ } catch (_) {
+ return { valid: false, feedback: Invalid URL!
};
+ }
+ }
+
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}
/>
;
+ const Tooltip = sdk.getComponent("elements.Tooltip");
+ tooltip = ;
}
const icon = this.props.iconPath ?
diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js
index c6a2113adb..eb3fcf272d 100644
--- a/src/components/views/elements/Field.js
+++ b/src/components/views/elements/Field.js
@@ -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 .
};
+ 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 = {prefix};
}
- 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
+ // handle displaying feedback on validity
+ const Tooltip = sdk.getComponent("elements.Tooltip");
+ let feedback;
+ if (this.state.feedback) {
+ feedback =
;
+ }
+
+ return
{prefixContainer}
{fieldInput}
+ {feedback}
;
}
}
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index f5ee60a2d8..ef9864358b 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -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 ?
-
:
+
:
;
const contextButton = this.state.hover || this.state.menuDisplayed ?
diff --git a/src/components/views/elements/ToolTipButton.js b/src/components/views/elements/ToolTipButton.js
index b5b2d735ee..239095f196 100644
--- a/src/components/views/elements/ToolTipButton.js
+++ b/src/components/views/elements/ToolTipButton.js
@@ -39,8 +39,8 @@ module.exports = React.createClass({
},
render: function() {
- const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
- const tip = this.state.hover ?
-
+
{ this.props.label }
);
diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js
index 44441f4754..8482bce593 100644
--- a/src/components/views/groups/GroupInviteTile.js
+++ b/src/components/views/groups/GroupInviteTile.js
@@ -143,8 +143,8 @@ export default React.createClass({
let tooltip;
if (this.props.collapsed && this.state.hover) {
- const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
- tooltip =
;
+ const Tooltip = sdk.getComponent("elements.Tooltip");
+ tooltip =
;
}
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js
index 55263ef7b7..6a4128dfa7 100644
--- a/src/components/views/messages/MStickerBody.js
+++ b/src/components/views/messages/MStickerBody.js
@@ -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
-
+
;
}
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js
index f9e9d64b9e..4bf160007e 100644
--- a/src/components/views/rooms/RoomTile.js
+++ b/src/components/views/rooms/RoomTile.js
@@ -364,8 +364,8 @@ module.exports = React.createClass({
label =
{ name };
}
} else if (this.state.hover) {
- const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
- tooltip =
;
+ const Tooltip = sdk.getComponent("elements.Tooltip");
+ tooltip =
;
}
//var incomingCallBox;