From 3bddad6857bb071d8ccd4d67f3a76810b6d2666e Mon Sep 17 00:00:00 2001
From: Riad Benguella <benguella@gmail.com>
Date: Thu, 1 Jun 2017 10:49:45 +0100
Subject: [PATCH 1/8] Chrome: Allow adding, removing tags from posts

---
 .eslintrc.json                                |   1 +
 components/form-token-field/style.scss        |   4 +-
 components/index.js                           |   1 +
 editor/sidebar/post-taxonomies/style.scss     |   5 +
 .../sidebar/post-taxonomies/tags-selector.js  | 108 ++++++++++++++++--
 5 files changed, 105 insertions(+), 14 deletions(-)

diff --git a/.eslintrc.json b/.eslintrc.json
index 4fe4696451600..763603865944f 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -8,6 +8,7 @@
 	],
 	"env": {
 		"browser": false,
+		"es6": true,
 		"node": true,
 		"mocha": true
 	},
diff --git a/components/form-token-field/style.scss b/components/form-token-field/style.scss
index 21dba50794231..2c496f6413928 100644
--- a/components/form-token-field/style.scss
+++ b/components/form-token-field/style.scss
@@ -127,7 +127,7 @@ input[type="text"].components-form-token-field__input {
 }
 
 .components-form-token-field__token-text,
-.components-form-token-field__remove-token {
+.components-form-token-field__remove-token.components-icon-button {
 	display: inline-block;
 	line-height: 24px;
 	background: $dark-gray-500;
@@ -142,7 +142,7 @@ input[type="text"].components-form-token-field__input {
 	text-overflow: ellipsis;
 }
 
-.components-form-token-field__remove-token {
+.components-form-token-field__remove-token.components-icon-button {
 	cursor: pointer;
 	border-radius: 0 4px 4px 0;
 	padding: 0 2px;
diff --git a/components/index.js b/components/index.js
index 1582340f6b380..5a7e9876cf3f8 100644
--- a/components/index.js
+++ b/components/index.js
@@ -1,6 +1,7 @@
 export { default as Button } from './button';
 export { default as Dashicon } from './dashicon';
 export { default as FormToggle } from './form-toggle';
+export { default as FormTokenField } from './form-token-field';
 export { default as HtmlEmbed } from './html-embed';
 export { default as IconButton } from './icon-button';
 export { default as Panel } from './panel';
diff --git a/editor/sidebar/post-taxonomies/style.scss b/editor/sidebar/post-taxonomies/style.scss
index 79756bf2ec3a4..e6331b7cf749d 100644
--- a/editor/sidebar/post-taxonomies/style.scss
+++ b/editor/sidebar/post-taxonomies/style.scss
@@ -1,3 +1,8 @@
 .editor-post-taxonomies__tags-selector {
 	margin-top: 10px;
+
+	.spinner {
+		float: none;
+		margin: 0;
+	}
 }
diff --git a/editor/sidebar/post-taxonomies/tags-selector.js b/editor/sidebar/post-taxonomies/tags-selector.js
index f0918e3ce6445..568e9761a498e 100644
--- a/editor/sidebar/post-taxonomies/tags-selector.js
+++ b/editor/sidebar/post-taxonomies/tags-selector.js
@@ -1,36 +1,120 @@
+/**
+ * External dependencies
+ */
+import { connect } from 'react-redux';
+import { unescape, find } from 'lodash';
+
 /**
  * WordPress dependencies
  */
 import { Component } from 'element';
-import FormTokenField from 'components/form-token-field';
+import { Spinner, FormTokenField } from 'components';
+import { getEditedPostAttribute } from '../../selectors';
+import { editPost } from '../../actions';
+
+const DEFAULT_TAGS_QUERY = {
+	number: 1000,
+	order_by: 'count',
+	order: 'DESC',
+};
+const MAX_TERMS_SUGGESTIONS = 20;
 
 class TagsSelector extends Component {
 	constructor() {
 		super( ...arguments );
-		this.onTokensChange = this.onTokensChange.bind( this );
+		this.onTagsChange = this.onTagsChange.bind( this );
 		this.state = {
-			tokens: [ 'React', 'Vue' ],
+			loading: true,
+			availableTags: [],
 		};
 	}
 
-	onTokensChange( value ) {
-		this.setState( { tokens: value } );
+	componentDidMount() {
+		this.fetchTagsRequest = new wp.api.collections.Tags().fetch( DEFAULT_TAGS_QUERY )
+			.done( ( tags ) => {
+				this.setState( {
+					loading: false,
+					availableTags: tags,
+				} );
+			} )
+			.fail( ( xhr ) => {
+				if ( xhr.statusText === 'abort' ) {
+					return;
+				}
+				this.setState( {
+					loading: false,
+				} );
+			} );
+	}
+
+	componentWillUnmount() {
+		if ( this.fetchTagsRequest ) {
+			this.fetchTagsRequest.abort();
+		}
+	}
+
+	onTagsChange( tagNames ) {
+		const newTagNames = tagNames.filter( ( tagName ) =>
+			! find( this.state.availableTags, ( tag ) => tag.name === tagName )
+		);
+		const tagNamesToIds = ( names, availableTags ) => {
+			return names
+				.map( ( tagName ) =>
+					find( availableTags, ( tag ) => tag.name === tagName ).id
+				);
+		};
+
+		if ( newTagNames.length === 0 ) {
+			return this.props.onUpdateTags( tagNamesToIds( tagNames, this.state.availableTags ) );
+		}
+		const createTag = ( tagName ) => new wp.api.models.Tag( { name: tagName } ).save();
+		Promise
+			.all( newTagNames.map( createTag ) )
+			.then( ( newTags ) => {
+				const newAvailableTags = this.state.availableTags.concat( newTags );
+				this.setState( { availableTags: newAvailableTags } );
+				return this.props.onUpdateTags( tagNamesToIds( tagNames, newAvailableTags ) );
+			} );
 	}
 
 	render() {
-		const suggestions = [ 'React', 'Vue', 'Angular', 'Cycle', 'PReact', 'Inferno' ];
+		const { tags = [] } = this.props;
+		const { loading, availableTags } = this.state;
+		const selectedTags = tags.map( ( tagId ) => {
+			const tagObject = find( this.state.availableTags, ( tag ) => tag.id === tagId );
+			return tagObject ? tagObject.name : '';
+		} );
+		const tagNames = availableTags.map( ( tag ) => tag.name );
 
 		return (
 			<div className="editor-post-taxonomies__tags-selector">
-				<FormTokenField
-					value={ this.state.tokens }
-					suggestions={ suggestions }
-					onChange={ this.onTokensChange }
-				/>
+				{ loading && <Spinner /> }
+				{ ! loading &&
+					<FormTokenField
+						value={ selectedTags }
+						displayTransform={ unescape }
+						suggestions={ tagNames }
+						onChange={ this.onTagsChange }
+						maxSuggestions={ MAX_TERMS_SUGGESTIONS }
+					/>
+				}
 			</div>
 		);
 	}
 }
 
-export default TagsSelector;
+export default connect(
+	( state ) => {
+		return {
+			tags: getEditedPostAttribute( state, 'tags' ),
+		};
+	},
+	( dispatch ) => {
+		return {
+			onUpdateTags( tags ) {
+				dispatch( editPost( { tags } ) );
+			},
+		};
+	}
+)( TagsSelector );
 

From 4042170ade06e02da33d7e4538a6a63fd375d1ea Mon Sep 17 00:00:00 2001
From: Joen Asmussen <joen@automattic.com>
Date: Thu, 1 Jun 2017 12:27:17 +0200
Subject: [PATCH 2/8] Polish the token chips a bit.

---
 components/form-token-field/style.scss | 28 +++++++++++++-------------
 components/form-token-field/token.js   |  2 +-
 2 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/components/form-token-field/style.scss b/components/form-token-field/style.scss
index 2c496f6413928..f979edfc3a114 100644
--- a/components/form-token-field/style.scss
+++ b/components/form-token-field/style.scss
@@ -4,8 +4,9 @@
 	margin: 0;
 	padding: 0;
 	background-color: $white;
+    border-radius: 4px;
 	border: 1px solid $light-gray-500;
-	color: $dark-gray-800;
+	color: $dark-gray-700;
 	cursor: text;
 	transition: all .15s ease-in-out;
 
@@ -28,7 +29,7 @@
 	display: flex;
 	flex-wrap: wrap;
 	align-items: flex-start;
-	padding: 5px 14px 5px 0;
+	padding: 4px;
 }
 
 // Token input
@@ -37,13 +38,13 @@ input[type="text"].components-form-token-field__input {
 	width: auto;
 	max-width: 100%;
 	margin: 2px 0 2px 8px;
-	padding: 0 0 0 6px;
+	padding: 0;
 	line-height: 24px;
 	background: inherit;
 	border: 0;
 	outline: none;
 	font-family: inherit;
-	font-size: 14px;
+	font-size: $default-font-size;
 	color: $dark-gray-800;
 	box-shadow: none;
 
@@ -54,10 +55,10 @@ input[type="text"].components-form-token-field__input {
 
 // Tokens
 .components-form-token-field__token {
-	font-size: 14px;
+	font-size: $default-font-size;
 	display: flex;
-	margin: 2px 0 2px 8px;
-	color: $white;
+	margin: 2px 4px 2px 0;
+	color: $dark-gray-700;
 	overflow: hidden;
 
 	&.is-success {
@@ -130,13 +131,13 @@ input[type="text"].components-form-token-field__input {
 .components-form-token-field__remove-token.components-icon-button {
 	display: inline-block;
 	line-height: 24px;
-	background: $dark-gray-500;
+	background: $light-gray-500;
 	transition: all .2s cubic-bezier( .4, 1, .4, 1 );
 }
 
 .components-form-token-field__token-text {
-	border-radius: 4px 0 0 4px;
-	padding: 0 4px 0 6px;
+	border-radius: 12px 0 0 12px;
+	padding: 0 4px 0 8px;
 	white-space: nowrap;
 	overflow: hidden;
 	text-overflow: ellipsis;
@@ -144,15 +145,14 @@ input[type="text"].components-form-token-field__input {
 
 .components-form-token-field__remove-token.components-icon-button {
 	cursor: pointer;
-	border-radius: 0 4px 4px 0;
+	border-radius: 0 12px 12px 0;
 	padding: 0 2px;
 	font-size: 10px;
-	color: $light-gray-500;
+	color: $dark-gray-500;
 	line-height: 10px;
 
 	&:hover {
-		color: white;
-		background: $dark-gray-600;
+		color: $dark-gray-700;
 	}
 }
 
diff --git a/components/form-token-field/token.js b/components/form-token-field/token.js
index f42d46cb0759d..e16734d413435 100644
--- a/components/form-token-field/token.js
+++ b/components/form-token-field/token.js
@@ -43,7 +43,7 @@ function Token( {
 			</span>
 			<IconButton
 				className="components-form-token-field__remove-token"
-				icon="no-alt"
+				icon="dismiss"
 				onClick={ ! disabled && onClick }
 			/>
 		</span>

From deed57faedbc0cbc9eed559f66b11d2ab94c7610 Mon Sep 17 00:00:00 2001
From: Riad Benguella <benguella@gmail.com>
Date: Thu, 1 Jun 2017 11:56:13 +0100
Subject: [PATCH 3/8] Chrome: Optimistic update to the selected tags

---
 .../sidebar/post-taxonomies/tags-selector.js  | 26 ++++++++++++++-----
 1 file changed, 20 insertions(+), 6 deletions(-)

diff --git a/editor/sidebar/post-taxonomies/tags-selector.js b/editor/sidebar/post-taxonomies/tags-selector.js
index 568e9761a498e..ded442e42c634 100644
--- a/editor/sidebar/post-taxonomies/tags-selector.js
+++ b/editor/sidebar/post-taxonomies/tags-selector.js
@@ -26,6 +26,7 @@ class TagsSelector extends Component {
 		this.state = {
 			loading: true,
 			availableTags: [],
+			selectedTags: [],
 		};
 	}
 
@@ -36,6 +37,7 @@ class TagsSelector extends Component {
 					loading: false,
 					availableTags: tags,
 				} );
+				this.updateSelectedTags( this.props.tags );
 			} )
 			.fail( ( xhr ) => {
 				if ( xhr.statusText === 'abort' ) {
@@ -53,7 +55,24 @@ class TagsSelector extends Component {
 		}
 	}
 
+	componentWillReceiveProps( newProps ) {
+		if ( newProps.tags !== this.props.tags ) {
+			this.updateSelectedTags( newProps.tags );
+		}
+	}
+
+	updateSelectedTags( tags = [] ) {
+		const selectedTags = tags.map( ( tagId ) => {
+			const tagObject = find( this.state.availableTags, ( tag ) => tag.id === tagId );
+			return tagObject ? tagObject.name : '';
+		} );
+		this.setState( {
+			selectedTags,
+		} );
+	}
+
 	onTagsChange( tagNames ) {
+		this.setState( { selectedTags: tagNames } );
 		const newTagNames = tagNames.filter( ( tagName ) =>
 			! find( this.state.availableTags, ( tag ) => tag.name === tagName )
 		);
@@ -78,12 +97,7 @@ class TagsSelector extends Component {
 	}
 
 	render() {
-		const { tags = [] } = this.props;
-		const { loading, availableTags } = this.state;
-		const selectedTags = tags.map( ( tagId ) => {
-			const tagObject = find( this.state.availableTags, ( tag ) => tag.id === tagId );
-			return tagObject ? tagObject.name : '';
-		} );
+		const { loading, availableTags, selectedTags } = this.state;
 		const tagNames = availableTags.map( ( tag ) => tag.name );
 
 		return (

From 197c19b17c638a6a7541c72f2b7a02f99530cd10 Mon Sep 17 00:00:00 2001
From: Riad Benguella <benguella@gmail.com>
Date: Thu, 1 Jun 2017 12:01:41 +0100
Subject: [PATCH 4/8] Tests: Removing slow token field tests

---
 components/form-token-field/test/index.js | 95 -----------------------
 1 file changed, 95 deletions(-)

diff --git a/components/form-token-field/test/index.js b/components/form-token-field/test/index.js
index 0d0dceaf1d077..82585f702075b 100644
--- a/components/form-token-field/test/index.js
+++ b/components/form-token-field/test/index.js
@@ -197,36 +197,6 @@ describe( 'FormTokenField', function() {
 			setText( '  at  ' );
 			expect( getSuggestionsText() ).to.deep.equal( fixtures.matchingSuggestions.at );
 		} );
-
-		it( 'should manage the selected suggestion based on both keyboard and mouse events', test( function() {
-			// We need a high timeout here to accomodate Travis CI
-			this.timeout( 10000 );
-
-			setText( 't' );
-			expect( getSuggestionsText() ).to.deep.equal( fixtures.matchingSuggestions.t );
-			expect( getSelectedSuggestion() ).to.equal( null );
-			sendKeyDown( keyCodes.downArrow ); // 'the'
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'he' ] );
-			sendKeyDown( keyCodes.downArrow ); // 'to'
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'o' ] );
-
-			const hoverSuggestion = tokenFieldNode.find( '.components-form-token-field__suggestion' ).at( 5 ); // 'it'
-			expect( getSuggestionNodeText( hoverSuggestion ) ).to.deep.equal( [ 'i', 't' ] );
-
-			// before sending a hover event, we need to wait for
-			// SuggestionList#_scrollingIntoView to become false
-			this.clock.tick( 100 );
-
-			hoverSuggestion.simulate( 'mouseEnter' );
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 'i', 't' ] );
-			sendKeyDown( keyCodes.upArrow );
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 'wi', 't', 'h' ] );
-			sendKeyDown( keyCodes.upArrow );
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'his' ] );
-			hoverSuggestion.simulate( 'click' );
-			expect( getSelectedSuggestion() ).to.equal( null );
-			expect( getTokensHTML() ).to.deep.equal( [ 'foo', 'bar', 'it' ] );
-		} ) );
 	} );
 
 	describe( 'adding tokens', function() {
@@ -323,25 +293,6 @@ describe( 'FormTokenField', function() {
 			);
 		} ) );
 
-		it( 'should add the suggested token when the (non-blank) input field loses focus', test( function() {
-			testOnBlur(
-				't',                    // initialText
-				true,                   // selectSuggestion
-				[ 't', 'o' ],       // expectedSuggestion
-				[ 'foo', 'bar', 'to' ] // expectedTokens
-			);
-		} ) );
-
-		it( 'should not add the suggested token when the (blank) input field loses focus', test( function() {
-			testOnBlur(
-				'',               // initialText
-				true,             // selectSuggestion
-				'of',             // expectedSuggestion
-				[ 'foo', 'bar' ], // expectedTokens
-				this.clock
-			);
-		} ) );
-
 		it( 'should not lose focus when a suggestion is clicked', test( function() {
 			// prevents regression of https://github.com/Automattic/wp-calypso/issues/1884
 
@@ -362,52 +313,6 @@ describe( 'FormTokenField', function() {
 			sendKeyDown( keyCodes.tab );
 			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'baz', 'quux', 'bar' ] );
 		} );
-
-		it( 'should add tokens from the selected matching suggestion using Tab', function() {
-			setText( 't' );
-			expect( getSelectedSuggestion() ).to.equal( null );
-			sendKeyDown( keyCodes.downArrow ); // 'the'
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'he' ] );
-			sendKeyDown( keyCodes.downArrow ); // 'to'
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'o' ] );
-			sendKeyDown( keyCodes.tab );
-			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'bar', 'to' ] );
-			expect( getSelectedSuggestion() ).to.equal( null );
-		} );
-
-		it( 'should add tokens from the selected matching suggestion using Enter', function() {
-			setText( 't' );
-			expect( getSelectedSuggestion() ).to.equal( null );
-			sendKeyDown( keyCodes.downArrow ); // 'the'
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'he' ] );
-			sendKeyDown( keyCodes.downArrow ); // 'to'
-			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'o' ] );
-			sendKeyDown( keyCodes.enter );
-			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'bar', 'to' ] );
-			expect( getSelectedSuggestion() ).to.equal( null );
-		} );
-
-		it( 'should add tokens from the selected suggestion using Tab', function() {
-			expect( getSelectedSuggestion() ).to.equal( null );
-			sendKeyDown( keyCodes.downArrow ); // 'the'
-			expect( getSelectedSuggestion() ).to.equal( 'the' );
-			sendKeyDown( keyCodes.downArrow ); // 'of'
-			expect( getSelectedSuggestion() ).to.equal( 'of' );
-			sendKeyDown( keyCodes.tab );
-			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'bar', 'of' ] );
-			expect( getSelectedSuggestion() ).to.equal( null );
-		} );
-
-		it( 'should add tokens from the selected suggestion using Enter', function() {
-			expect( getSelectedSuggestion() ).to.equal( null );
-			sendKeyDown( keyCodes.downArrow ); // 'the'
-			expect( getSelectedSuggestion() ).to.equal( 'the' );
-			sendKeyDown( keyCodes.downArrow ); // 'of'
-			expect( getSelectedSuggestion() ).to.equal( 'of' );
-			sendKeyDown( keyCodes.enter );
-			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'bar', 'of' ] );
-			expect( getSelectedSuggestion() ).to.equal( null );
-		} );
 	} );
 
 	describe( 'adding multiple tokens when pasting', function() {

From 0fc2c3e0b5ecfbf020457dab9b06591978664d52 Mon Sep 17 00:00:00 2001
From: Riad Benguella <benguella@gmail.com>
Date: Thu, 1 Jun 2017 12:04:45 +0100
Subject: [PATCH 5/8] Components: Fix style indentation

---
 components/form-token-field/style.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/components/form-token-field/style.scss b/components/form-token-field/style.scss
index f979edfc3a114..29413b21e6b6c 100644
--- a/components/form-token-field/style.scss
+++ b/components/form-token-field/style.scss
@@ -4,7 +4,7 @@
 	margin: 0;
 	padding: 0;
 	background-color: $white;
-    border-radius: 4px;
+	border-radius: 4px;
 	border: 1px solid $light-gray-500;
 	color: $dark-gray-700;
 	cursor: text;

From 8f20d1d4509592554b7a7b31e12e64c1845996c7 Mon Sep 17 00:00:00 2001
From: Riad Benguella <benguella@gmail.com>
Date: Thu, 1 Jun 2017 16:58:02 +0100
Subject: [PATCH 6/8] Chrome: Fix the tags selector query

---
 editor/sidebar/post-taxonomies/tags-selector.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/editor/sidebar/post-taxonomies/tags-selector.js b/editor/sidebar/post-taxonomies/tags-selector.js
index ded442e42c634..3692a129c65cc 100644
--- a/editor/sidebar/post-taxonomies/tags-selector.js
+++ b/editor/sidebar/post-taxonomies/tags-selector.js
@@ -13,8 +13,8 @@ import { getEditedPostAttribute } from '../../selectors';
 import { editPost } from '../../actions';
 
 const DEFAULT_TAGS_QUERY = {
-	number: 1000,
-	order_by: 'count',
+	number: -1,
+	orderby: 'count',
 	order: 'DESC',
 };
 const MAX_TERMS_SUGGESTIONS = 20;

From bbb9eaf3fdd3e566bc22f1095cd38e8395f79de1 Mon Sep 17 00:00:00 2001
From: Riad Benguella <benguella@gmail.com>
Date: Thu, 1 Jun 2017 17:03:47 +0100
Subject: [PATCH 7/8] Chrome: Disable the tags input when loading

---
 editor/sidebar/post-taxonomies/style.scss     |  5 -----
 .../sidebar/post-taxonomies/tags-selector.js  | 20 +++++++++----------
 2 files changed, 9 insertions(+), 16 deletions(-)

diff --git a/editor/sidebar/post-taxonomies/style.scss b/editor/sidebar/post-taxonomies/style.scss
index e6331b7cf749d..79756bf2ec3a4 100644
--- a/editor/sidebar/post-taxonomies/style.scss
+++ b/editor/sidebar/post-taxonomies/style.scss
@@ -1,8 +1,3 @@
 .editor-post-taxonomies__tags-selector {
 	margin-top: 10px;
-
-	.spinner {
-		float: none;
-		margin: 0;
-	}
 }
diff --git a/editor/sidebar/post-taxonomies/tags-selector.js b/editor/sidebar/post-taxonomies/tags-selector.js
index 3692a129c65cc..9d1af1ac29cc7 100644
--- a/editor/sidebar/post-taxonomies/tags-selector.js
+++ b/editor/sidebar/post-taxonomies/tags-selector.js
@@ -8,7 +8,7 @@ import { unescape, find } from 'lodash';
  * WordPress dependencies
  */
 import { Component } from 'element';
-import { Spinner, FormTokenField } from 'components';
+import { FormTokenField } from 'components';
 import { getEditedPostAttribute } from '../../selectors';
 import { editPost } from '../../actions';
 
@@ -102,16 +102,14 @@ class TagsSelector extends Component {
 
 		return (
 			<div className="editor-post-taxonomies__tags-selector">
-				{ loading && <Spinner /> }
-				{ ! loading &&
-					<FormTokenField
-						value={ selectedTags }
-						displayTransform={ unescape }
-						suggestions={ tagNames }
-						onChange={ this.onTagsChange }
-						maxSuggestions={ MAX_TERMS_SUGGESTIONS }
-					/>
-				}
+				<FormTokenField
+					value={ selectedTags }
+					displayTransform={ unescape }
+					suggestions={ tagNames }
+					onChange={ this.onTagsChange }
+					maxSuggestions={ MAX_TERMS_SUGGESTIONS }
+					disabled={ loading }
+				/>
 			</div>
 		);
 	}

From 0905da46a9bf57638748dd398ec5f2056bccf0dc Mon Sep 17 00:00:00 2001
From: Riad Benguella <benguella@gmail.com>
Date: Thu, 1 Jun 2017 17:29:20 +0100
Subject: [PATCH 8/8] Revert "Tests: Removing slow token field tests"

This reverts commit d384b451149a6d971bece019591fc6c655a83572.
---
 components/form-token-field/test/index.js | 95 +++++++++++++++++++++++
 1 file changed, 95 insertions(+)

diff --git a/components/form-token-field/test/index.js b/components/form-token-field/test/index.js
index 82585f702075b..0d0dceaf1d077 100644
--- a/components/form-token-field/test/index.js
+++ b/components/form-token-field/test/index.js
@@ -197,6 +197,36 @@ describe( 'FormTokenField', function() {
 			setText( '  at  ' );
 			expect( getSuggestionsText() ).to.deep.equal( fixtures.matchingSuggestions.at );
 		} );
+
+		it( 'should manage the selected suggestion based on both keyboard and mouse events', test( function() {
+			// We need a high timeout here to accomodate Travis CI
+			this.timeout( 10000 );
+
+			setText( 't' );
+			expect( getSuggestionsText() ).to.deep.equal( fixtures.matchingSuggestions.t );
+			expect( getSelectedSuggestion() ).to.equal( null );
+			sendKeyDown( keyCodes.downArrow ); // 'the'
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'he' ] );
+			sendKeyDown( keyCodes.downArrow ); // 'to'
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'o' ] );
+
+			const hoverSuggestion = tokenFieldNode.find( '.components-form-token-field__suggestion' ).at( 5 ); // 'it'
+			expect( getSuggestionNodeText( hoverSuggestion ) ).to.deep.equal( [ 'i', 't' ] );
+
+			// before sending a hover event, we need to wait for
+			// SuggestionList#_scrollingIntoView to become false
+			this.clock.tick( 100 );
+
+			hoverSuggestion.simulate( 'mouseEnter' );
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 'i', 't' ] );
+			sendKeyDown( keyCodes.upArrow );
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 'wi', 't', 'h' ] );
+			sendKeyDown( keyCodes.upArrow );
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'his' ] );
+			hoverSuggestion.simulate( 'click' );
+			expect( getSelectedSuggestion() ).to.equal( null );
+			expect( getTokensHTML() ).to.deep.equal( [ 'foo', 'bar', 'it' ] );
+		} ) );
 	} );
 
 	describe( 'adding tokens', function() {
@@ -293,6 +323,25 @@ describe( 'FormTokenField', function() {
 			);
 		} ) );
 
+		it( 'should add the suggested token when the (non-blank) input field loses focus', test( function() {
+			testOnBlur(
+				't',                    // initialText
+				true,                   // selectSuggestion
+				[ 't', 'o' ],       // expectedSuggestion
+				[ 'foo', 'bar', 'to' ] // expectedTokens
+			);
+		} ) );
+
+		it( 'should not add the suggested token when the (blank) input field loses focus', test( function() {
+			testOnBlur(
+				'',               // initialText
+				true,             // selectSuggestion
+				'of',             // expectedSuggestion
+				[ 'foo', 'bar' ], // expectedTokens
+				this.clock
+			);
+		} ) );
+
 		it( 'should not lose focus when a suggestion is clicked', test( function() {
 			// prevents regression of https://github.com/Automattic/wp-calypso/issues/1884
 
@@ -313,6 +362,52 @@ describe( 'FormTokenField', function() {
 			sendKeyDown( keyCodes.tab );
 			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'baz', 'quux', 'bar' ] );
 		} );
+
+		it( 'should add tokens from the selected matching suggestion using Tab', function() {
+			setText( 't' );
+			expect( getSelectedSuggestion() ).to.equal( null );
+			sendKeyDown( keyCodes.downArrow ); // 'the'
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'he' ] );
+			sendKeyDown( keyCodes.downArrow ); // 'to'
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'o' ] );
+			sendKeyDown( keyCodes.tab );
+			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'bar', 'to' ] );
+			expect( getSelectedSuggestion() ).to.equal( null );
+		} );
+
+		it( 'should add tokens from the selected matching suggestion using Enter', function() {
+			setText( 't' );
+			expect( getSelectedSuggestion() ).to.equal( null );
+			sendKeyDown( keyCodes.downArrow ); // 'the'
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'he' ] );
+			sendKeyDown( keyCodes.downArrow ); // 'to'
+			expect( getSelectedSuggestion() ).to.deep.equal( [ 't', 'o' ] );
+			sendKeyDown( keyCodes.enter );
+			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'bar', 'to' ] );
+			expect( getSelectedSuggestion() ).to.equal( null );
+		} );
+
+		it( 'should add tokens from the selected suggestion using Tab', function() {
+			expect( getSelectedSuggestion() ).to.equal( null );
+			sendKeyDown( keyCodes.downArrow ); // 'the'
+			expect( getSelectedSuggestion() ).to.equal( 'the' );
+			sendKeyDown( keyCodes.downArrow ); // 'of'
+			expect( getSelectedSuggestion() ).to.equal( 'of' );
+			sendKeyDown( keyCodes.tab );
+			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'bar', 'of' ] );
+			expect( getSelectedSuggestion() ).to.equal( null );
+		} );
+
+		it( 'should add tokens from the selected suggestion using Enter', function() {
+			expect( getSelectedSuggestion() ).to.equal( null );
+			sendKeyDown( keyCodes.downArrow ); // 'the'
+			expect( getSelectedSuggestion() ).to.equal( 'the' );
+			sendKeyDown( keyCodes.downArrow ); // 'of'
+			expect( getSelectedSuggestion() ).to.equal( 'of' );
+			sendKeyDown( keyCodes.enter );
+			expect( wrapper.state( 'tokens' ) ).to.deep.equal( [ 'foo', 'bar', 'of' ] );
+			expect( getSelectedSuggestion() ).to.equal( null );
+		} );
 	} );
 
 	describe( 'adding multiple tokens when pasting', function() {