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:

committed by
Andrew Bonventre

parent
0a59ac2565
commit
6df7fbf431
@@ -42,7 +42,9 @@ limitations under the License.
|
|||||||
account="[[account]]"
|
account="[[account]]"
|
||||||
class$="[[_computeChipClass(account)]]"
|
class$="[[_computeChipClass(account)]]"
|
||||||
data-account-id$="[[account._account_id]]"
|
data-account-id$="[[account._account_id]]"
|
||||||
removable="[[_computeRemovable(account)]]">
|
removable="[[_computeRemovable(account)]]"
|
||||||
|
on-keydown="_handleChipKeydown"
|
||||||
|
tabindex$="[[index]]">
|
||||||
</gr-account-chip>
|
</gr-account-chip>
|
||||||
</template>
|
</template>
|
||||||
<gr-account-entry
|
<gr-account-entry
|
||||||
|
@@ -90,6 +90,10 @@
|
|||||||
|
|
||||||
_handleRemove: function(e) {
|
_handleRemove: function(e) {
|
||||||
var toRemove = e.detail.account;
|
var toRemove = e.detail.account;
|
||||||
|
this._removeAccount(toRemove);
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeAccount: function(toRemove) {
|
||||||
for (var i = 0; i < this.accounts.length; i++) {
|
for (var i = 0; i < this.accounts.length; i++) {
|
||||||
var matches;
|
var matches;
|
||||||
var account = this.accounts[i];
|
var account = this.accounts[i];
|
||||||
@@ -104,8 +108,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.warn('received remove event for missing account',
|
console.warn('received remove event for missing account', toRemove);
|
||||||
e.detail.account);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_handleInputKeydown: function(e) {
|
_handleInputKeydown: function(e) {
|
||||||
@@ -118,6 +121,50 @@
|
|||||||
case 8: // Backspace
|
case 8: // Backspace
|
||||||
this.splice('accounts', this.accounts.length - 1, 1);
|
this.splice('accounts', this.accounts.length - 1, 1);
|
||||||
break;
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -256,6 +256,45 @@ limitations under the License.
|
|||||||
done();
|
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>
|
</script>
|
||||||
|
@@ -53,6 +53,15 @@ limitations under the License.
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
:host:focus {
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
:host:focus .container,
|
||||||
|
:host:focus gr-button {
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
.transparentBackground,
|
.transparentBackground,
|
||||||
gr-button.transparentBackground {
|
gr-button.transparentBackground {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -61,6 +70,7 @@ limitations under the License.
|
|||||||
<div class$="container [[_getBackgroundClass(transparentBackground)]]">
|
<div class$="container [[_getBackgroundClass(transparentBackground)]]">
|
||||||
<gr-account-link account="[[account]]"></gr-account-link>
|
<gr-account-link account="[[account]]"></gr-account-link>
|
||||||
<gr-button
|
<gr-button
|
||||||
|
id="remove"
|
||||||
hidden$="[[!removable]]" hidden
|
hidden$="[[!removable]]" hidden
|
||||||
class$="remove [[_getBackgroundClass(transparentBackground)]]"
|
class$="remove [[_getBackgroundClass(transparentBackground)]]"
|
||||||
on-tap="_handleRemoveTap">×</gr-button>
|
on-tap="_handleRemoveTap">×</gr-button>
|
||||||
|
@@ -18,6 +18,19 @@
|
|||||||
Polymer({
|
Polymer({
|
||||||
is: 'gr-account-chip',
|
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: {
|
properties: {
|
||||||
account: Object,
|
account: Object,
|
||||||
removable: {
|
removable: {
|
||||||
|
Reference in New Issue
Block a user