Make account list navigable by keyboard

Makes account chips focusable via tabindex attribute, and responds to
keypresses while they're focused.

Feature: Issue 4703
Change-Id: Id8f73e73b91afa0582f06cb981fd117d021accff
This commit is contained in:
Kasper Nilsson
2016-11-11 12:44:14 -08:00
committed by Andrew Bonventre
parent 0a59ac2565
commit 6df7fbf431
5 changed files with 114 additions and 3 deletions

View File

@@ -42,7 +42,9 @@ limitations under the License.
account="[[account]]"
class$="[[_computeChipClass(account)]]"
data-account-id$="[[account._account_id]]"
removable="[[_computeRemovable(account)]]">
removable="[[_computeRemovable(account)]]"
on-keydown="_handleChipKeydown"
tabindex$="[[index]]">
</gr-account-chip>
</template>
<gr-account-entry

View File

@@ -90,6 +90,10 @@
_handleRemove: function(e) {
var toRemove = e.detail.account;
this._removeAccount(toRemove);
},
_removeAccount: function(toRemove) {
for (var i = 0; i < this.accounts.length; i++) {
var matches;
var account = this.accounts[i];
@@ -104,8 +108,7 @@
return;
}
}
console.warn('received remove event for missing account',
e.detail.account);
console.warn('received remove event for missing account', toRemove);
},
_handleInputKeydown: function(e) {
@@ -118,6 +121,50 @@
case 8: // Backspace
this.splice('accounts', this.accounts.length - 1, 1);
break;
case 37: // Left arrow
var chips = this.accountChips;
if (chips[chips.length - 1]) {
chips[chips.length - 1].focus();
}
break;
}
},
_handleChipKeydown: function(e) {
var chip = e.target;
var chips = this.accountChips;
var index = chips.indexOf(chip);
switch (e.keyCode) {
case 8: // Backspace
case 13: // Enter
case 32: // Spacebar
case 46: // Delete
this._removeAccount(chip.account);
// Splice from this array to avoid inconsistent ordering of
// event handling.
chips.splice(index, 1);
if (index < chips.length) {
chips[index].focus();
} else if (index > 0) {
chips[index - 1].focus();
} else {
this.$.entry.focus();
}
break;
case 37: // Left arrow
if (index > 0) {
chip.blur();
chips[index - 1].focus();
}
break;
case 39: // Right arrow
chip.blur();
if (index < chips.length - 1) {
chips[index + 1].focus();
} else {
this.$.entry.focus();
}
break;
}
},

View File

@@ -256,6 +256,45 @@ limitations under the License.
done();
});
});
test('arrow key navigation', function(done) {
var input = element.$.entry.$.input;
input.text = '';
element.accounts = [makeAccount(), makeAccount()];
MockInteractions.focus(input.$.input);
flush(function() {
var chips = element.accountChips;
var chipsOneSpy = sandbox.spy(chips[1], 'focus');
MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
assert.isTrue(chipsOneSpy.called);
var chipsZeroSpy = sandbox.spy(chips[0], 'focus');
MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left
assert.isTrue(chipsZeroSpy.called);
MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left
assert.isTrue(chipsZeroSpy.calledOnce);
MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right
assert.isTrue(chipsOneSpy.calledTwice);
done();
});
});
test('delete', function(done) {
element.accounts = [makeAccount(), makeAccount()];
flush(function() {
var chips = element.accountChips;
var focusSpy = sandbox.spy(element.accountChips[1], 'focus');
var removeSpy = sandbox.spy(element, '_removeAccount');
MockInteractions.pressAndReleaseKeyOn(
element.accountChips[0], 8); // Backspace
assert.isTrue(focusSpy.called);
assert.isTrue(removeSpy.calledOnce);
MockInteractions.pressAndReleaseKeyOn(
element.accountChips[1], 46); // Delete
assert.isTrue(removeSpy.calledTwice);
done();
});
});
});
});
</script>

View File

@@ -53,6 +53,15 @@ limitations under the License.
padding: 0;
text-decoration: none;
}
:host:focus {
border-color: transparent;
box-shadow: none;
outline: none;
}
:host:focus .container,
:host:focus gr-button {
background: #ccc;
}
.transparentBackground,
gr-button.transparentBackground {
background-color: transparent;
@@ -61,6 +70,7 @@ limitations under the License.
<div class$="container [[_getBackgroundClass(transparentBackground)]]">
<gr-account-link account="[[account]]"></gr-account-link>
<gr-button
id="remove"
hidden$="[[!removable]]" hidden
class$="remove [[_getBackgroundClass(transparentBackground)]]"
on-tap="_handleRemoveTap">×</gr-button>

View File

@@ -18,6 +18,19 @@
Polymer({
is: 'gr-account-chip',
/**
* Fired to indicate a key was pressed while this chip was focused.
*
* @event account-chip-keydown
*/
/**
* Fired to indicate this chip should be removed, i.e. when the x button is
* clicked or when the remove function is called.
*
* @event remove
*/
properties: {
account: Object,
removable: {