From 40f16fa310d85053235e3c79ea950e559d6a33da Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 1 Feb 2019 00:36:19 +0100 Subject: [PATCH] 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. --- res/css/_components.scss | 2 +- res/css/views/elements/_Field.scss | 33 +++++++++++ .../_Tooltip.scss} | 57 ++++++++++++------- res/themes/dark/css/_dark.scss | 1 + res/themes/light/css/_light.scss | 3 +- src/components/structures/BottomLeftMenu.js | 4 +- src/components/views/auth/ServerConfig.js | 10 ++++ src/components/views/elements/ActionButton.js | 4 +- src/components/views/elements/Field.js | 53 ++++++++++++++++- src/components/views/elements/TagTile.js | 4 +- .../views/elements/ToolTipButton.js | 4 +- .../RoomTooltip.js => elements/Tooltip.js} | 14 ++--- .../views/groups/GroupInviteTile.js | 4 +- src/components/views/messages/MStickerBody.js | 4 +- src/components/views/rooms/RoomTile.js | 4 +- 15 files changed, 154 insertions(+), 47 deletions(-) rename res/css/views/{rooms/_RoomTooltip.scss => elements/_Tooltip.scss} (54%) rename src/components/views/{rooms/RoomTooltip.js => elements/Tooltip.js} (89%) 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;