From 24fe7c049454fcf46e1a0aaf550e4be8fc833ee4 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 18 Jul 2018 19:32:39 +0200 Subject: [PATCH 1/3] Tags autocompleter keyboard interaction improvements. --- .../components/src/form-token-field/index.js | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/components/src/form-token-field/index.js b/packages/components/src/form-token-field/index.js index 0612f51683ecd4..7e3a6d87e9d9e8 100644 --- a/packages/components/src/form-token-field/index.js +++ b/packages/components/src/form-token-field/index.js @@ -10,6 +10,7 @@ import classnames from 'classnames'; import { __, _n, sprintf } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { withInstanceId } from '@wordpress/compose'; +import { BACKSPACE, ENTER, UP, DOWN, LEFT, RIGHT, SPACE, DELETE, ESCAPE } from '@wordpress/keycodes'; /** * Internal dependencies @@ -105,32 +106,36 @@ class FormTokenField extends Component { let preventDefault = false; switch ( event.keyCode ) { - case 8: // backspace (delete to left) + case BACKSPACE: preventDefault = this.handleDeleteKey( this.deleteTokenBeforeInput ); break; - case 13: // enter/return + case ENTER: preventDefault = this.addCurrentToken(); break; - case 37: // left arrow + case LEFT: preventDefault = this.handleLeftArrowKey(); break; - case 38: // up arrow + case UP: preventDefault = this.handleUpArrowKey(); break; - case 39: // right arrow + case RIGHT: preventDefault = this.handleRightArrowKey(); break; - case 40: // down arrow + case DOWN: preventDefault = this.handleDownArrowKey(); break; - case 46: // delete (to right) + case DELETE: preventDefault = this.handleDeleteKey( this.deleteTokenAfterInput ); break; - case 32: // space + case SPACE: if ( this.props.tokenizeOnSpace ) { preventDefault = this.addCurrentToken(); } break; + case ESCAPE: + preventDefault = this.handleEscapeKey(); + event.stopPropagation(); + break; default: break; } @@ -251,8 +256,16 @@ class FormTokenField extends Component { } handleUpArrowKey() { - this.setState( ( state ) => ( { - selectedSuggestionIndex: Math.max( ( state.selectedSuggestionIndex || 0 ) - 1, 0 ), + this.setState( ( state, props ) => ( { + selectedSuggestionIndex: ( + ( state.selectedSuggestionIndex === 0 ? this.getMatchingSuggestions( + state.incompleteTokenValue, + props.suggestions, + props.value, + props.maxSuggestions, + props.saveTransform + ).length : state.selectedSuggestionIndex ) - 1 + ), selectedSuggestionScroll: true, } ) ); @@ -261,15 +274,14 @@ class FormTokenField extends Component { handleDownArrowKey() { this.setState( ( state, props ) => ( { - selectedSuggestionIndex: Math.min( - ( state.selectedSuggestionIndex + 1 ) || 0, - this.getMatchingSuggestions( + selectedSuggestionIndex: ( + ( state.selectedSuggestionIndex + 1 ) % this.getMatchingSuggestions( state.incompleteTokenValue, props.suggestions, props.value, props.maxSuggestions, props.saveTransform - ).length - 1 + ).length ), selectedSuggestionScroll: true, } ) ); @@ -277,6 +289,11 @@ class FormTokenField extends Component { return true; // preventDefault } + handleEscapeKey() { + this.setState( initialState ); + return true; // preventDefault + } + handleCommaKey() { if ( this.inputHasValidValue() ) { this.addNewToken( this.state.incompleteTokenValue ); From 80c6e9e2f58a87521f6dcaa59bdef003b2cc73cc Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 25 Jul 2018 18:06:12 +0200 Subject: [PATCH 2/3] Keep entered text when pressing Escape. --- .../components/src/form-token-field/index.js | 30 +++++++++++++------ .../src/form-token-field/test/index.js | 24 +++++++++++++-- .../test/lib/token-field-wrapper.js | 3 +- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/components/src/form-token-field/index.js b/packages/components/src/form-token-field/index.js index 7e3a6d87e9d9e8..b662442cd839fe 100644 --- a/packages/components/src/form-token-field/index.js +++ b/packages/components/src/form-token-field/index.js @@ -27,6 +27,7 @@ const initialState = { isExpanded: false, selectedSuggestionIndex: -1, selectedSuggestionScroll: false, + showSuggestions: false, }; class FormTokenField extends Component { @@ -133,7 +134,7 @@ class FormTokenField extends Component { } break; case ESCAPE: - preventDefault = this.handleEscapeKey(); + preventDefault = this.handleEscapeKey( event ); event.stopPropagation(); break; default: @@ -193,6 +194,9 @@ class FormTokenField extends Component { const separator = this.props.tokenizeOnSpace ? /[ ,\t]+/ : /[,\t]+/; const items = text.split( separator ); const tokenValue = last( items ) || ''; + const inputHasMinimumChars = tokenValue.trim().length > 1; + const matchingSuggestions = this.getMatchingSuggestions( tokenValue ); + const hasVisibleSuggestions = inputHasMinimumChars && !! matchingSuggestions.length; if ( items.length > 1 ) { this.addNewTokens( items.slice( 0, -1 ) ); @@ -203,15 +207,16 @@ class FormTokenField extends Component { selectedSuggestionIndex: -1, selectedSuggestionScroll: false, isExpanded: false, + showSuggestions: false, } ); this.props.onInputChange( tokenValue ); - const inputHasMinimumChars = tokenValue.trim().length > 1; if ( inputHasMinimumChars ) { - const matchingSuggestions = this.getMatchingSuggestions( tokenValue ); - - this.setState( { isExpanded: !! matchingSuggestions.length } ); + this.setState( { + isExpanded: hasVisibleSuggestions, + showSuggestions: hasVisibleSuggestions, + } ); if ( !! matchingSuggestions.length ) { this.props.debouncedSpeak( sprintf( _n( @@ -289,8 +294,14 @@ class FormTokenField extends Component { return true; // preventDefault } - handleEscapeKey() { - this.setState( initialState ); + handleEscapeKey( event ) { + this.setState( { + incompleteTokenValue: event.target.value, + isExpanded: false, + selectedSuggestionIndex: -1, + selectedSuggestionScroll: false, + showSuggestions: false, + } ); return true; // preventDefault } @@ -379,6 +390,8 @@ class FormTokenField extends Component { incompleteTokenValue: '', selectedSuggestionIndex: -1, selectedSuggestionScroll: false, + isExpanded: false, + showSuggestions: false, } ); if ( this.state.isActive ) { @@ -527,6 +540,7 @@ class FormTokenField extends Component { instanceId, className, } = this.props; + const { showSuggestions } = this.state; const classes = classnames( className, 'components-form-token-field', { 'is-active': this.state.isActive, 'is-disabled': disabled, @@ -537,8 +551,6 @@ class FormTokenField extends Component { tabIndex: '-1', }; const matchingSuggestions = this.getMatchingSuggestions(); - const inputHasMinimumChars = this.state.incompleteTokenValue.trim().length > 1; - const showSuggestions = inputHasMinimumChars && !! matchingSuggestions.length; if ( ! disabled ) { tokenFieldProps = Object.assign( {}, tokenFieldProps, { diff --git a/packages/components/src/form-token-field/test/index.js b/packages/components/src/form-token-field/test/index.js index ea476f8c6997f1..c7e6d6e67ee753 100644 --- a/packages/components/src/form-token-field/test/index.js +++ b/packages/components/src/form-token-field/test/index.js @@ -118,12 +118,16 @@ describe( 'FormTokenField', function() { describe( 'displaying tokens', function() { it( 'should render default tokens', function() { + wrapper.setState( { + showSuggestions: true, + } ); expect( wrapper.state.tokens ).toEqual( [ 'foo', 'bar' ] ); } ); it( 'should display tokens with escaped special characters properly', function() { wrapper.setState( { tokens: fixtures.specialTokens.textEscaped, + showSuggestions: true, } ); expect( getTokensHTML() ).toEqual( fixtures.specialTokens.htmlEscaped ); } ); @@ -137,6 +141,7 @@ describe( 'FormTokenField', function() { // through unescaped to the HTML. wrapper.setState( { tokens: fixtures.specialTokens.textUnescaped, + showSuggestions: true, } ); expect( getTokensHTML() ).toEqual( fixtures.specialTokens.htmlUnescaped ); } ); @@ -144,6 +149,9 @@ describe( 'FormTokenField', function() { describe( 'suggestions', function() { it( 'should not render suggestions unless we type at least two characters', function() { + wrapper.setState( { + showSuggestions: true, + } ); expect( getSuggestionsText() ).toEqual( [] ); setText( 'th' ); expect( getSuggestionsText() ).toEqual( fixtures.matchingSuggestions.th ); @@ -157,23 +165,28 @@ describe( 'FormTokenField', function() { } ); it( 'suggestions that begin with match are boosted', function() { + wrapper.setState( { + showSuggestions: true, + } ); setText( 'so' ); expect( getSuggestionsText() ).toEqual( fixtures.matchingSuggestions.so ); } ); it( 'should match against the unescaped values of suggestions with special characters', function() { - setText( '& S' ); wrapper.setState( { tokenSuggestions: fixtures.specialSuggestions.textUnescaped, + showSuggestions: true, } ); + setText( '& S' ); expect( getSuggestionsText() ).toEqual( fixtures.specialSuggestions.matchAmpersandUnescaped ); } ); it( 'should match against the unescaped values of suggestions with special characters (including spaces)', function() { - setText( 's &' ); wrapper.setState( { tokenSuggestions: fixtures.specialSuggestions.textUnescaped, + showSuggestions: true, } ); + setText( 's &' ); expect( getSuggestionsText() ).toEqual( fixtures.specialSuggestions.matchAmpersandSequence ); } ); @@ -181,16 +194,23 @@ describe( 'FormTokenField', function() { setText( 'amp' ); wrapper.setState( { tokenSuggestions: fixtures.specialSuggestions.textUnescaped, + showSuggestions: true, } ); expect( getSuggestionsText() ).toEqual( fixtures.specialSuggestions.matchAmpersandEscaped ); } ); it( 'should match suggestions even with trailing spaces', function() { + wrapper.setState( { + showSuggestions: true, + } ); setText( ' at ' ); expect( getSuggestionsText() ).toEqual( fixtures.matchingSuggestions.at ); } ); it( 'should manage the selected suggestion based on both keyboard and mouse events', function() { + wrapper.setState( { + showSuggestions: true, + } ); setText( 'th' ); expect( getSuggestionsText() ).toEqual( fixtures.matchingSuggestions.th ); expect( getSelectedSuggestion() ).toBe( null ); diff --git a/packages/components/src/form-token-field/test/lib/token-field-wrapper.js b/packages/components/src/form-token-field/test/lib/token-field-wrapper.js index 1e3fb2811a8348..bb12a6894b634d 100644 --- a/packages/components/src/form-token-field/test/lib/token-field-wrapper.js +++ b/packages/components/src/form-token-field/test/lib/token-field-wrapper.js @@ -26,6 +26,7 @@ class TokenFieldWrapper extends Component { this.state = { tokenSuggestions: suggestions, tokens: Object.freeze( [ 'foo', 'bar' ] ), + showSuggestions: false, }; this.onTokensChange = this.onTokensChange.bind( this ); } @@ -33,7 +34,7 @@ class TokenFieldWrapper extends Component { render() { return ( Date: Mon, 30 Jul 2018 19:37:09 +0200 Subject: [PATCH 3/3] Remove showSuggestions in favor of isExpanded. --- .../components/src/form-token-field/index.js | 9 ++------- .../src/form-token-field/test/index.js | 20 +++++++++---------- .../test/lib/token-field-wrapper.js | 4 ++-- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/components/src/form-token-field/index.js b/packages/components/src/form-token-field/index.js index b662442cd839fe..8b4a7ac56e0749 100644 --- a/packages/components/src/form-token-field/index.js +++ b/packages/components/src/form-token-field/index.js @@ -27,7 +27,6 @@ const initialState = { isExpanded: false, selectedSuggestionIndex: -1, selectedSuggestionScroll: false, - showSuggestions: false, }; class FormTokenField extends Component { @@ -207,7 +206,6 @@ class FormTokenField extends Component { selectedSuggestionIndex: -1, selectedSuggestionScroll: false, isExpanded: false, - showSuggestions: false, } ); this.props.onInputChange( tokenValue ); @@ -215,7 +213,6 @@ class FormTokenField extends Component { if ( inputHasMinimumChars ) { this.setState( { isExpanded: hasVisibleSuggestions, - showSuggestions: hasVisibleSuggestions, } ); if ( !! matchingSuggestions.length ) { @@ -300,7 +297,6 @@ class FormTokenField extends Component { isExpanded: false, selectedSuggestionIndex: -1, selectedSuggestionScroll: false, - showSuggestions: false, } ); return true; // preventDefault } @@ -391,7 +387,6 @@ class FormTokenField extends Component { selectedSuggestionIndex: -1, selectedSuggestionScroll: false, isExpanded: false, - showSuggestions: false, } ); if ( this.state.isActive ) { @@ -540,7 +535,7 @@ class FormTokenField extends Component { instanceId, className, } = this.props; - const { showSuggestions } = this.state; + const { isExpanded } = this.state; const classes = classnames( className, 'components-form-token-field', { 'is-active': this.state.isActive, 'is-disabled': disabled, @@ -578,7 +573,7 @@ class FormTokenField extends Component { { this.renderTokensAndInput() } - { showSuggestions && ( + { isExpanded && (