diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index b6035e5859..a4b5bb51d0 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -27,6 +27,15 @@ limitations under the License. white-space: nowrap; } + @keyframes visualbell { + from { background-color: #faa; } + to { background-color: $primary-bg-color; } + } + + &.mx_BasicMessageComposer_input_error { + animation: 0.2s visualbell; + } + .mx_BasicMessageComposer_input { white-space: pre-wrap; word-wrap: break-word; diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 4aa622b6c2..19304ec557 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -14,6 +14,8 @@ 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 classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; import EditorModel from '../../../editor/model'; @@ -271,7 +273,7 @@ export default class BasicMessageEditor extends React.Component { return; // don't preventDefault on anything else } } else if (event.key === "Tab") { - this._tabCompleteName(event); + this._tabCompleteName(); handled = true; } } @@ -281,20 +283,30 @@ export default class BasicMessageEditor extends React.Component { } } - async _tabCompleteName(event) { - const {model} = this.props; - const caret = this.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - const range = model.startRange(position); - range.expandBackwardsWhile((index, offset, part) => { - return part.text[offset] !== " " && (part.type === "plain" || part.type === "pill-candidate"); - }); - const {partCreator} = model; - await model.transform(() => { - const addedLen = range.replace([partCreator.pillCandidate(range.text)]); - return model.positionForOffset(caret.offset + addedLen, true); - }); - await model.autoComplete.onTab(); + async _tabCompleteName() { + try { + await new Promise(resolve => this.setState({showVisualBell: false}, resolve)); + const {model} = this.props; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + const range = model.startRange(position); + range.expandBackwardsWhile((index, offset, part) => { + return part.text[offset] !== " " && (part.type === "plain" || part.type === "pill-candidate"); + }); + const {partCreator} = model; + // await for auto-complete to be open + await model.transform(() => { + const addedLen = range.replace([partCreator.pillCandidate(range.text)]); + return model.positionForOffset(caret.offset + addedLen, true); + }); + await model.autoComplete.onTab(); + if (!model.autoComplete.hasSelection()) { + this.setState({showVisualBell: true}); + model.autoComplete.close(); + } + } catch (err) { + console.error(err); + } } isModified() { @@ -324,7 +336,14 @@ export default class BasicMessageEditor extends React.Component { // not really, but we could not serialize the parts, and just change the autoCompleter partCreator.setAutoCompleteCreator(autoCompleteCreator( () => this._autocompleteRef, - query => new Promise(resolve => this.setState({query}, resolve)), + query => { + return new Promise(resolve => this.setState({query}, resolve)); + // if setState + // if (this.state.query === query) { + // return Promise.resolve(); + // } else { + // } + }, )); this.historyManager = new HistoryManager(partCreator); // initial render of model @@ -365,7 +384,10 @@ export default class BasicMessageEditor extends React.Component { /> ); } - return (
+ const classes = classNames("mx_BasicMessageComposer", { + "mx_BasicMessageComposer_input_error": this.state.showVisualBell, + }); + return (
{ autoComplete }