Cleanup: use iron-a11y-keys-behavior for keyboard shortcuts

+ This does not cover on-keydown handlers within elements.
  A follow-up change will account for those.
+ Keyboard shortcuts are disabled within gr-overlay, input,
  and textarea elements.
+ Added tests for new behavior (plus some missing ones covering
  broken behavior).
+ Removed blur hacks used on elements to placate the kb
  shortcuts due to restrictions that have been removed.

Bug: Issue 4198
Change-Id: Ide8009a3bfc340a35a8ec8b9189a85b49c8a95aa
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
index f636650..ba7d3de1 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
@@ -14,69 +14,30 @@
 limitations under the License.
 -->
 <link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
+
 <script>
 (function(window) {
   'use strict';
 
-  /** @polymerBehavior Gerrit.KeyboardShortcutBehavior */
-  var KeyboardShortcutBehavior = {
-    // Set of identifiers currently blocking keyboard shortcuts. Stored as
-    // a map of string to the value of true.
-    _disablers: {},
-
-    properties: {
-      keyEventTarget: {
-        type: Object,
-        value: function() { return this; },
-      },
-
-      _boundKeyHandler: {
-        type: Function,
-        readonly: true,
-        value: function() { return this._handleKey.bind(this); },
-      },
-    },
-
-    attached: function() {
-      this.keyEventTarget.addEventListener('keydown', this._boundKeyHandler);
-    },
-
-    detached: function() {
-      this.keyEventTarget.removeEventListener('keydown', this._boundKeyHandler);
-    },
-
+  var KeyboardShortcutBehaviorImpl = {
     shouldSuppressKeyboardShortcut: function(e) {
-      for (var c in KeyboardShortcutBehavior._disablers) {
-        if (KeyboardShortcutBehavior._disablers[c] === true) {
-          return true;
-        }
+      e = Polymer.dom(e.detail ? e.detail.keyboardEvent : e);
+      if (e.path[0].tagName === 'INPUT' || e.path[0].tagName === 'TEXTAREA') {
+        return true;
       }
-      var getModifierState = e.getModifierState ?
-          e.getModifierState.bind(e) :
-          function() { return false; };
-      var target = e.detail ? e.detail.keyboardEvent : e.target;
-      return getModifierState('Control') ||
-             getModifierState('Alt') ||
-             getModifierState('Meta') ||
-             getModifierState('Fn') ||
-             target.tagName == 'INPUT' ||
-             target.tagName == 'TEXTAREA' ||
-             target.tagName == 'SELECT' ||
-             target.tagName == 'BUTTON' ||
-             target.tagName == 'A' ||
-             target.tagName == 'GR-BUTTON';
-    },
-
-    disable: function(id) {
-      KeyboardShortcutBehavior._disablers[id] = true;
-    },
-
-    enable: function(id) {
-      delete KeyboardShortcutBehavior._disablers[id];
+      for (var i = 0; i < e.path.length; i++) {
+        if (e.path[i].tagName === 'GR-OVERLAY') { return true; }
+      }
+      return false;
     },
   };
 
   window.Gerrit = window.Gerrit || {};
-  window.Gerrit.KeyboardShortcutBehavior = KeyboardShortcutBehavior;
+  /** @polymerBehavior Gerrit.KeyboardShortcutBehavior */
+  window.Gerrit.KeyboardShortcutBehavior = [
+    Polymer.IronA11yKeysBehavior,
+    KeyboardShortcutBehaviorImpl,
+  ];
 })(window);
 </script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior_test.html
index 5ec4145..457c5f25 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior_test.html
@@ -30,49 +30,76 @@
   </template>
 </test-fixture>
 
+<test-fixture id="within-overlay">
+  <template>
+    <gr-overlay>
+      <test-element></test-element>
+    </gr-overlay>
+  </template>
+</test-fixture>
+
 <script>
   suite('keyboard-shortcut-behavior tests', function() {
     var element;
+    var overlay;
 
     suiteSetup(function() {
       // Define a Polymer element that uses this behavior.
       Polymer({
         is: 'test-element',
         behaviors: [Gerrit.KeyboardShortcutBehavior],
-        properties: {
-          keyEventTarget: {
-            value: function() { return document.body; },
-          },
-          log: {
-            value: function() { return []; },
-          },
+        keyBindings: {
+          'k': '_handleKey'
         },
-
-        _handleKey: function(e) {
-          if (!this.shouldSuppressKeyboardShortcut(e)) {
-            this.log.push(e.keyCode);
-          }
-        },
+        _handleKey: function() {},
       });
     });
 
     setup(function() {
       element = fixture('basic');
+      overlay = fixture('within-overlay');
     });
 
-    test('blocks keydown events iff one or more disablers', function() {
-      MockInteractions.pressAndReleaseKeyOn(document.body, 97);  // 'a'
-      Gerrit.KeyboardShortcutBehavior.enable('x');  // should have no effect
-      MockInteractions.pressAndReleaseKeyOn(document.body, 98);  // 'b'
-      Gerrit.KeyboardShortcutBehavior.disable('x');  // blocking starts here
-      MockInteractions.pressAndReleaseKeyOn(document.body, 99);  // 'c'
-      Gerrit.KeyboardShortcutBehavior.disable('y');
-      MockInteractions.pressAndReleaseKeyOn(document.body, 100);  // 'd'
-      Gerrit.KeyboardShortcutBehavior.enable('x');
-      MockInteractions.pressAndReleaseKeyOn(document.body, 101);  // 'e'
-      Gerrit.KeyboardShortcutBehavior.enable('y');  // blocking ends here
-      MockInteractions.pressAndReleaseKeyOn(document.body, 102);  // 'f'
-      assert.deepEqual(element.log, [97, 98, 102]);
+    test('doesn’t block kb shortcuts for non-whitelisted els', function(done) {
+      var divEl = document.createElement('div');
+      element.appendChild(divEl);
+      element._handleKey = function(e) {
+        assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
+        done();
+      };
+      MockInteractions.keyDownOn(divEl, 75, null, 'k');
     });
+
+    test('blocks kb shortcuts for input els', function(done) {
+      var inputEl = document.createElement('input');
+      element.appendChild(inputEl);
+      element._handleKey = function(e) {
+        assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+        done();
+      };
+      MockInteractions.keyDownOn(inputEl, 75, null, 'k');
+    });
+
+    test('blocks kb shortcuts for textarea els', function(done) {
+      var textareaEl = document.createElement('textarea');
+      element.appendChild(textareaEl);
+      element._handleKey = function(e) {
+        assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+        done();
+      };
+      MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
+    });
+
+    test('blocks kb shortcuts for anything in a gr-overlay', function(done) {
+      var divEl = document.createElement('div');
+      var element = overlay.querySelector('test-element');
+      element.appendChild(divEl);
+      element._handleKey = function(e) {
+        assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
+        done();
+      };
+      MockInteractions.keyDownOn(divEl, 75, null, 'k');
+    });
+
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 2be0afc..51626cc 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -78,6 +78,12 @@
       Gerrit.RESTClientBehavior,
     ],
 
+    keyBindings: {
+      'j': '_handleJKey',
+      'k': '_handleKKey',
+      'o enter': '_handleEnterKey',
+    },
+
     attached: function() {
       this._loadPreferences();
     },
@@ -149,31 +155,37 @@
           account._account_id != change.owner._account_id;
     },
 
-    _handleKey: function(e) {
-      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
-      if (this.groups == null) { return; }
+    _getAggregateGroupsLen: function(groups) {
+      groups = groups || [];
       var len = 0;
       this.groups.forEach(function(group) {
         len += group.length;
       });
-      switch (e.keyCode) {
-        case 74:  // 'j'
-          e.preventDefault();
-          if (this.selectedIndex == len - 1) { return; }
-          this.selectedIndex += 1;
-          break;
-        case 75:  // 'k'
-          e.preventDefault();
-          if (this.selectedIndex == 0) { return; }
-          this.selectedIndex -= 1;
-          break;
-        case 79:  // 'o'
-        case 13:  // 'enter'
-          e.preventDefault();
-          page.show(this._changeURLForIndex(this.selectedIndex));
-          break;
-      }
+      return len;
+    },
+
+    _handleJKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      var len = this._getAggregateGroupsLen(this.groups);
+      if (this.selectedIndex === len - 1) { return; }
+      this.selectedIndex += 1;
+    },
+
+    _handleKKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      if (this.selectedIndex === 0) { return; }
+      this.selectedIndex -= 1;
+    },
+
+    _handleEnterKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      page.show(this._changeURLForIndex(this.selectedIndex));
     },
 
     _changeURLForIndex: function(index) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 718cfb5..33b4279 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -131,25 +131,25 @@
 
       flush(function() {
         assert.isTrue(elementItems[0].selected);
-        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+        MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
         assert.equal(element.selectedIndex, 1);
-        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+        MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
 
         var showStub = sinon.stub(page, 'show');
         assert.equal(element.selectedIndex, 2);
-        MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'enter'
+        MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
         assert(showStub.lastCall.calledWithExactly('/c/2/'),
             'Should navigate to /c/2/');
 
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
         assert.equal(element.selectedIndex, 1);
-        MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'enter'
+        MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
         assert(showStub.lastCall.calledWithExactly('/c/1/'),
             'Should navigate to /c/1/');
 
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
         assert.equal(element.selectedIndex, 0);
 
         showStub.restore();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index bbd4d2d..485f155 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -110,6 +110,13 @@
       '_paramsAndChangeChanged(params, _change)',
     ],
 
+    keyBindings: {
+      'shift+r': '_handleCapitalRKey',
+      'a': '_handleAKey',
+      'd': '_handleDKey',
+      'u': '_handleUKey',
+    },
+
     attached: function() {
       this._getLoggedIn().then(function(loggedIn) {
         this._loggedIn = loggedIn;
@@ -579,30 +586,29 @@
       }.bind(this));
     },
 
-    _handleKey: function(e) {
+    _handleAKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-      switch (e.keyCode) {
-        case 65:  // 'a'
-          if (this._loggedIn && !e.shiftKey) {
-            e.preventDefault();
-            this._openReplyDialog();
-          }
-          break;
-        case 68: // 'd'
-          e.preventDefault();
-          this.$.downloadOverlay.open();
-          break;
-        case 82: // 'r'
-          if (e.shiftKey) {
-            e.preventDefault();
-            this._switchToMostRecentPatchNum();
-          }
-          break;
-        case 85:  // 'u'
-          e.preventDefault();
-          this._determinePageBack();
-          break;
-      }
+      if (!this._loggedIn || e.detail.keyboardEvent.shiftKey) { return; }
+      e.preventDefault();
+      this._openReplyDialog();
+    },
+
+    _handleDKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      e.preventDefault();
+      this.$.downloadOverlay.open();
+    },
+
+    _handleCapitalRKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      e.preventDefault();
+      this._switchToMostRecentPatchNum();
+    },
+
+    _handleUKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      e.preventDefault();
+      this._determinePageBack();
     },
 
     _determinePageBack: function() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 7c8a329..4fd27ae 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -53,27 +53,27 @@
     suite('keyboard shortcuts', function() {
       test('U should navigate to / if no backPage set', function() {
         var showStub = sandbox.stub(page, 'show');
-        MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'U'
+        MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
         assert(showStub.lastCall.calledWithExactly('/'));
       });
 
       test('U should navigate to backPage if set', function() {
         element.backPage = '/dashboard/self';
         var showStub = sandbox.stub(page, 'show');
-        MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'U'
+        MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
         assert(showStub.lastCall.calledWithExactly('/dashboard/self'));
       });
 
       test('A should toggle overlay', function() {
-        MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
+        MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
         var overlayEl = element.$.replyOverlay;
         assert.isFalse(overlayEl.opened);
         element._loggedIn = true;
 
-        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
+        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
         assert.isFalse(overlayEl.opened);
 
-        MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
+        MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
         assert.isTrue(overlayEl.opened);
         overlayEl.close();
         assert.isFalse(overlayEl.opened);
@@ -117,13 +117,12 @@
           done();
         });
 
-        // 'shift + R'
-        MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift');
+        MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
       });
 
       test('d should open download overlay', function() {
         var stub = sandbox.stub(element.$.downloadOverlay, 'open');
-        MockInteractions.pressAndReleaseKeyOn(element, 68); // 'd'
+        MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
         assert.isTrue(stub.called);
       });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 873ab3b..4e68559 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -179,8 +179,7 @@
         <select
             id="modeSelect"
             is="gr-select"
-            bind-value="{{diffViewMode}}"
-            on-change="_handleDropdownChange">
+            bind-value="{{diffViewMode}}">
           <option value="SIDE_BY_SIDE">Side By Side</option>
           <option value="UNIFIED_DIFF">Unified</option>
         </select>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 98c4284..ab889ac 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -106,6 +106,22 @@
       Gerrit.URLEncodingBehavior,
     ],
 
+    keyBindings: {
+      'shift+left': '_handleShiftLeftKey',
+      'shift+right': '_handleShiftRightKey',
+      'i': '_handleIKey',
+      'shift+i': '_handleCapitalIKey',
+      'down j': '_handleDownKey',
+      'up k': '_handleUpKey',
+      'c': '_handleCKey',
+      '[': '_handleLeftBracketKey',
+      ']': '_handleRightBracketKey',
+      'o enter': '_handleEnterKey',
+      'n': '_handleNKey',
+      'p': '_handlePKey',
+      'shift+a': '_handleCapitalAKey',
+    },
+
     reload: function() {
       if (!this.changeNum || !this.patchRange.patchNum) {
         return Promise.resolve();
@@ -216,9 +232,6 @@
         this.set(['_shownFiles', i, '__expanded'], true);
         this.set(['_files', i, '__expanded'], true);
       }
-      if (e && e.target) {
-        e.target.blur();
-      }
     },
 
     _collapseAllDiffs: function(e) {
@@ -228,9 +241,6 @@
         this.set(['_files', i, '__expanded'], false);
       }
       this.$.cursor.handleDiffUpdate();
-      if (e && e.target) {
-        e.target.blur();
-      }
     },
 
     _computeCommentsString: function(comments, patchNum, path) {
@@ -298,113 +308,137 @@
           });
     },
 
-    _handleKey: function(e) {
+    _handleShiftLeftKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-      switch (e.keyCode) {
-        case 37: // left
-          if (e.shiftKey && this._showInlineDiffs) {
-            e.preventDefault();
-            this.$.cursor.moveLeft();
-          }
-          break;
-        case 39: // right
-          if (e.shiftKey && this._showInlineDiffs) {
-            e.preventDefault();
-            this.$.cursor.moveRight();
-          }
-          break;
-        case 73:  // 'i'
-          if (e.shiftKey) {
-            e.preventDefault();
-            this._toggleInlineDiffs();
-          } else if (this.selectedIndex !== undefined) {
-            e.preventDefault();
-            var expanded = this._files[this.selectedIndex].__expanded;
-            // Until Polymer 2.0, manual management of reflection between _files
-            // and _shownFiles is necessary.
-            this.set(['_shownFiles', this.selectedIndex, '__expanded'],
-                !expanded);
-            this.set(['_files', this.selectedIndex, '__expanded'], !expanded);
-          }
-          break;
-        case 40:  // down
-        case 74:  // 'j'
-          e.preventDefault();
-          if (this._showInlineDiffs) {
-            this.$.cursor.moveDown();
-          } else {
-            this.selectedIndex =
-                Math.min(this._numFilesShown, this.selectedIndex + 1);
-            this._scrollToSelectedFile();
-          }
-          break;
-        case 38:  // up
-        case 75:  // 'k'
-          e.preventDefault();
-          if (this._showInlineDiffs) {
-            this.$.cursor.moveUp();
-          } else {
-            this.selectedIndex = Math.max(0, this.selectedIndex - 1);
-            this._scrollToSelectedFile();
-          }
-          break;
-        case 67: // 'c'
-          var isRangeSelected = this.diffs.some(function(diff) {
-            return diff.isRangeSelected();
-          }, this);
-          if (this._showInlineDiffs && !isRangeSelected) {
-            e.preventDefault();
-            this._addDraftAtTarget();
-          }
-          break;
-        case 219:  // '['
-          e.preventDefault();
-          this._openSelectedFile(this._files.length - 1);
-          break;
-        case 221:  // ']'
-          e.preventDefault();
-          this._openSelectedFile(0);
-          break;
-        case 13:  // <enter>
-        case 79:  // 'o'
-          e.preventDefault();
-          if (this._showInlineDiffs) {
-            this._openCursorFile();
-          } else {
-            this._openSelectedFile();
-          }
-          break;
-        case 78:  // 'n'
-          if (this._showInlineDiffs) {
-            e.preventDefault();
-            if (e.shiftKey) {
-              this.$.cursor.moveToNextCommentThread();
-            } else {
-              this.$.cursor.moveToNextChunk();
-            }
-          }
-          break;
-        case 80:  // 'p'
-          if (this._showInlineDiffs) {
-            e.preventDefault();
-            if (e.shiftKey) {
-              this.$.cursor.moveToPreviousCommentThread();
-            } else {
-              this.$.cursor.moveToPreviousChunk();
-            }
-          }
-          break;
-        case 65:  // 'a'
-          if (e.shiftKey) { // Hide left diff.
-            e.preventDefault();
-            this._forEachDiff(function(diff) {
-              diff.toggleLeftDiff();
-            });
-          }
-          break;
+      if (!this._showInlineDiffs) { return; }
+
+      e.preventDefault();
+      this.$.cursor.moveLeft();
+    },
+
+    _handleShiftRightKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      if (!this._showInlineDiffs) { return; }
+
+      e.preventDefault();
+      this.$.cursor.moveRight();
+    },
+
+    _handleIKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      if (this.selectedIndex === undefined) { return; }
+
+      e.preventDefault();
+      var expanded = this._files[this.selectedIndex].__expanded;
+      // Until Polymer 2.0, manual management of reflection between _files
+      // and _shownFiles is necessary.
+      this.set(['_shownFiles', this.selectedIndex, '__expanded'],
+          !expanded);
+      this.set(['_files', this.selectedIndex, '__expanded'], !expanded);
+    },
+
+    _handleCapitalIKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this._toggleInlineDiffs();
+    },
+
+    _handleDownKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      if (this._showInlineDiffs) {
+        this.$.cursor.moveDown();
+      } else {
+        this.selectedIndex =
+            Math.min(this._numFilesShown, this.selectedIndex + 1);
+        this._scrollToSelectedFile();
       }
     },
 
+    _handleUpKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      if (this._showInlineDiffs) {
+        this.$.cursor.moveUp();
+      } else {
+        this.selectedIndex = Math.max(0, this.selectedIndex - 1);
+        this._scrollToSelectedFile();
+      }
+    },
+
+    _handleCKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      var isRangeSelected = this.diffs.some(function(diff) {
+        return diff.isRangeSelected();
+      }, this);
+      if (this._showInlineDiffs && !isRangeSelected) {
+        e.preventDefault();
+        this._addDraftAtTarget();
+      }
+    },
+
+    _handleLeftBracketKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this._openSelectedFile(this._files.length - 1);
+    },
+
+    _handleRightBracketKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this._openSelectedFile(0);
+    },
+
+    _handleEnterKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      if (this._showInlineDiffs) {
+        this._openCursorFile();
+      } else {
+        this._openSelectedFile();
+      }
+    },
+
+    _handleNKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      if (!this._showInlineDiffs) { return; }
+
+      e.preventDefault();
+      if (e.shiftKey) {
+        this.$.cursor.moveToNextCommentThread();
+      } else {
+        this.$.cursor.moveToNextChunk();
+      }
+    },
+
+    _handlePKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      if (!this._showInlineDiffs) { return; }
+
+      e.preventDefault();
+      if (e.shiftKey) {
+        this.$.cursor.moveToPreviousCommentThread();
+      } else {
+        this.$.cursor.moveToPreviousChunk();
+      }
+    },
+
+    _handleCapitalAKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this._forEachDiff(function(diff) {
+        diff.toggleLeftDiff();
+      });
+    },
+
     _toggleInlineDiffs: function() {
       if (this._showInlineDiffs) {
         this._collapseAllDiffs();
@@ -555,10 +589,6 @@
       return DiffViewMode.SIDE_BY_SIDE;
     },
 
-    _handleDropdownChange: function(e) {
-      e.target.blur();
-    },
-
     _fileListActionsVisible: function(numFilesShown, maxFilesForBulkActions) {
       return numFilesShown <= maxFilesForBulkActions;
     },
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index b530bae..1e146b5 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -155,7 +155,7 @@
             return [{toggleLeftDiff: toggleLeftDiffStub}];
           },
         });
-        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
+        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
         assert.isTrue(toggleLeftDiffStub.calledOnce);
         diffsStub.restore();
       });
@@ -168,25 +168,25 @@
         assert.isTrue(elementItems[0].hasAttribute('selected'));
         assert.isFalse(elementItems[1].hasAttribute('selected'));
         assert.isFalse(elementItems[2].hasAttribute('selected'));
-        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'J'
+        MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
         assert.equal(element.selectedIndex, 1);
-        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'J'
+        MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
 
         var showStub = sandbox.stub(page, 'show');
         assert.equal(element.selectedIndex, 2);
-        MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'ENTER'
+        MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
         assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
             'Should navigate to /c/42/2/myfile.txt');
 
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
+        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
         assert.equal(element.selectedIndex, 1);
-        MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'O'
+        MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o');
         assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
             'Should navigate to /c/42/2/file_added_in_rev2.txt');
 
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
+        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
+        MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
         assert.equal(element.selectedIndex, 0);
 
         showStub.restore();
@@ -194,23 +194,23 @@
 
       test('i key shows/hides selected inline diff', function() {
         element.selectedIndex = 0;
-        MockInteractions.pressAndReleaseKeyOn(element, 73);  // 'I'
+        MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
         flushAsynchronousOperations();
         assert.isFalse(element.diffs[0].hasAttribute('hidden'));
-        MockInteractions.pressAndReleaseKeyOn(element, 73);  // 'I'
+        MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
         flushAsynchronousOperations();
         assert.isTrue(element.diffs[0].hasAttribute('hidden'));
         element.selectedIndex = 1;
-        MockInteractions.pressAndReleaseKeyOn(element, 73);  // 'I'
+        MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
         flushAsynchronousOperations();
         assert.isFalse(element.diffs[1].hasAttribute('hidden'));
 
-        MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift');  // 'I'
+        MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
         flushAsynchronousOperations();
         for (var index in element.diffs) {
           assert.isFalse(element.diffs[index].hasAttribute('hidden'));
         }
-        MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift');  // 'I'
+        MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i');
         flushAsynchronousOperations();
         for (var index in element.diffs) {
           assert.isTrue(element.diffs[index].hasAttribute('hidden'));
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index cee2a69..aa0703f 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -113,17 +113,11 @@
       for (var i = 0; i < messageEls.length; i++) {
         messageEls[i].expanded = this._expanded;
       }
-      if (e && e.target) {
-        e.target.blur();
-      }
     },
 
     _handleAutomatedMessageToggleTap: function(e) {
       e.preventDefault();
       this._hideAutomated = !this._hideAutomated;
-      if (e && e.target) {
-        e.target.blur();
-      }
     },
 
     _handleScrollTo: function(e) {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 2c8549d..61eb280 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -94,6 +94,10 @@
       'searchButton.tap': '_preventDefaultAndNavigateToInputVal',
     },
 
+    keyBindings: {
+      '/': '_handleForwardSlashKey',
+    },
+
     properties: {
       value: {
         type: String,
@@ -292,18 +296,12 @@
           });
     },
 
-    _handleKey: function(e) {
+    _handleForwardSlashKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-      switch (e.keyCode) {
-        case 191:  // '/' or '?' with shift key.
-          // TODO(andybons): Localization using e.key/keypress event.
-          if (e.shiftKey) { break; }
-          e.preventDefault();
-          var s = this.$.searchInput;
-          s.focus();
-          s.setSelectionRange(0, s.value.length);
-          break;
-      }
+
+      e.preventDefault();
+      this.$.searchInput.focus();
+      this.$.searchInput.selectAll();
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index cd218d4..621511f 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -70,13 +70,15 @@
         done();
       });
       element.value = 'test';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
+      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+          null, 'enter');
     });
 
     test('search query should be double-escaped', function() {
       var showStub = sinon.stub(page, 'show');
       element.$.searchInput.text = 'fate/stay';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
+      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+          null, 'enter');
       assert.equal(showStub.lastCall.args[0], '/q/fate%252Fstay');
       showStub.restore();
     });
@@ -85,7 +87,8 @@
       var showStub = sinon.stub(page, 'show');
       var blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur');
       element.$.searchInput.text = 'fate/stay';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
+      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+          null, 'enter');
       assert.isTrue(blurSpy.called);
       showStub.restore();
       blurSpy.restore();
@@ -94,10 +97,19 @@
     test('empty search query does not trigger nav', function() {
       var showSpy = sinon.spy(page, 'show');
       element.value = '';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
+      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
+          null, 'enter');
       assert.isFalse(showSpy.called);
     });
 
+    test('keyboard shortcuts', function() {
+      var focusSpy = sinon.spy(element.$.searchInput, 'focus');
+      var selectAllSpy = sinon.spy(element.$.searchInput, 'selectAll');
+      MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/');
+      assert.isTrue(focusSpy.called);
+      assert.isTrue(selectAllSpy.called);
+    });
+
     suite('_getSearchSuggestions',
         function() {
       setup(function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index 20b032d..a24100f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -57,6 +57,10 @@
       '_commentsChanged(comments.splices)',
     ],
 
+    keyBindings: {
+      'e shift+e': '_handleEKey',
+    },
+
     attached: function() {
       this._getLoggedIn().then(function(loggedIn) {
         this._showActions = loggedIn;
@@ -88,12 +92,12 @@
       this._orderedComments = this._sortedComments(this.comments);
     },
 
-    _handleKey: function(e) {
+    _handleEKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-      if (e.keyCode === 69) { // 'e'
-        e.preventDefault();
-        this._expandCollapseComments(e.shiftKey);
-      }
+
+      // Don’t preventDefault in this case because it will render the event
+      // useless for other handlers (other gr-diff-comment-thread elements).
+      this._expandCollapseComments(e.detail.keyboardEvent.shiftKey);
     },
 
     _expandCollapseComments: function(actionIsCollapse) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index 73e4983..7b85b62 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -280,10 +280,10 @@
         }];
       element.comments = comments;
       var expandCollapseStub = sinon.stub(element, '_expandCollapseComments');
-      MockInteractions.pressAndReleaseKeyOn(element, 69);  // 'e'
+      MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
       assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
 
-      MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift');  // 'e'
+      MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
       assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
       expandCollapseStub.restore();
     });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 410a813..720e6542 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -279,34 +279,34 @@
     },
 
     _handleReply: function(e) {
-      this._preventDefaultAndBlur(e);
+      e.preventDefault();
       this.fire('reply', this._getEventPayload(), {bubbles: false});
     },
 
     _handleQuote: function(e) {
-      this._preventDefaultAndBlur(e);
+      e.preventDefault();
       this.fire(
           'reply', this._getEventPayload({quote: true}), {bubbles: false});
     },
 
     _handleDone: function(e) {
-      this._preventDefaultAndBlur(e);
+      e.preventDefault();
       this.fire('done', this._getEventPayload(), {bubbles: false});
     },
 
     _handleEdit: function(e) {
-      this._preventDefaultAndBlur(e);
+      e.preventDefault();
       this._messageText = this.comment.message;
       this.editing = true;
     },
 
     _handleSave: function(e) {
-      this._preventDefaultAndBlur(e);
+      e.preventDefault();
       this.save();
     },
 
     _handleCancel: function(e) {
-      this._preventDefaultAndBlur(e);
+      e.preventDefault();
       if (this.comment.message == null || this.comment.message.length == 0) {
         this._fireDiscard();
         return;
@@ -321,7 +321,7 @@
     },
 
     _handleDiscard: function(e) {
-      this._preventDefaultAndBlur(e);
+      e.preventDefault();
       if (!this.comment.__draft) {
         throw Error('Cannot discard a non-draft comment.');
       }
@@ -345,11 +345,6 @@
           }.bind(this));
     },
 
-    _preventDefaultAndBlur: function(e) {
-      e.preventDefault();
-      Polymer.dom(e).rootTarget.blur();
-    },
-
     _saveDraft: function(draft) {
       return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft);
     },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 9dbb25b..b6471e7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -217,8 +217,7 @@
               id="modeSelect"
               is="gr-select"
               bind-value="{{changeViewState.diffMode}}"
-              hidden$="[[_computeModeSelectHidden(_isImageDiff)]]"
-              on-change="_handleDropdownChange">
+              hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">
             <option value="SIDE_BY_SIDE">Side By Side</option>
             <option value="UNIFIED_DIFF">Unified</option>
           </select>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 44204fd..1d63b47 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -100,6 +100,22 @@
       '_getFiles(_changeNum, _patchRange.*)',
     ],
 
+    keyBindings: {
+      'esc': '_handleEscKey',
+      'shift+left': '_handleShiftLeftKey',
+      'shift+right': '_handleShiftRightKey',
+      'up k': '_handleUpKey',
+      'down j': '_handleDownKey',
+      'c': '_handleCKey',
+      '[': '_handleLeftBracketKey',
+      ']': '_handleRightBracketKey',
+      'n shift+n': '_handleNKey',
+      'p shift+p': '_handlePKey',
+      'a shift+a': '_handleAKey',
+      'u': '_handleUKey',
+      ',': '_handleCommaKey',
+    },
+
     attached: function() {
       this._getLoggedIn().then(function(loggedIn) {
         this._loggedIn = loggedIn;
@@ -185,103 +201,129 @@
           this._patchRange.patchNum, this._path, reviewed);
     },
 
-    _checkForModifiers: function(e) {
-      return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || false;
-    },
-
-    _handleKey: function(e) {
+    _handleEscKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
 
-      switch (e.keyCode) {
-        case 27: // escape
-          e.preventDefault();
-          this.$.diff.displayLine = false;
-          break;
-        case 37: // left
-          if (e.shiftKey) {
-            e.preventDefault();
-            this.$.cursor.moveLeft();
-          }
-          break;
-        case 39: // right
-          if (e.shiftKey) {
-            e.preventDefault();
-            this.$.cursor.moveRight();
-          }
-          break;
-        case 40: // down
-        case 74: // 'j'
-          e.preventDefault();
-          this.$.diff.displayLine = true;
-          this.$.cursor.moveDown();
-          break;
-        case 38: // up
-        case 75: // 'k'
-          e.preventDefault();
-          this.$.diff.displayLine = true;
-          this.$.cursor.moveUp();
-          break;
-        case 67: // 'c'
-          if (this._checkForModifiers(e)) { return; }
-          if (!this.$.diff.isRangeSelected()) {
-            e.preventDefault();
-            var line = this.$.cursor.getTargetLineElement();
-            if (line) {
-              this.$.diff.addDraftAtLine(line);
-            }
-          }
-          break;
-        case 219:  // '['
-          e.preventDefault();
-          this._navToFile(this._path, this._fileList, -1);
-          break;
-        case 221:  // ']'
-          e.preventDefault();
-          this._navToFile(this._path, this._fileList, 1);
-          break;
-        case 78:  // 'n'
-          e.preventDefault();
-          if (e.shiftKey) {
-            this.$.cursor.moveToNextCommentThread();
-          } else {
-            this.$.cursor.moveToNextChunk();
-          }
-          break;
-        case 80:  // 'p'
-          e.preventDefault();
-          if (e.shiftKey) {
-            this.$.cursor.moveToPreviousCommentThread();
-          } else {
-            this.$.cursor.moveToPreviousChunk();
-          }
-          break;
-        case 65:  // 'a'
-          if (e.shiftKey) { // Hide left diff.
-            e.preventDefault();
-            this.$.diff.toggleLeftDiff();
-            break;
-          }
+      e.preventDefault();
+      this.$.diff.displayLine = false;
+    },
 
-          if (!this._loggedIn) { break; }
+    _handleShiftLeftKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
 
-          this.set('changeViewState.showReplyDialog', true);
-          /* falls through */ // required by JSHint
-        case 85:  // 'u'
-          if (this._changeNum && this._patchRange.patchNum) {
-            e.preventDefault();
-            page.show(this._getChangePath(
-                this._changeNum,
-                this._patchRange,
-                this._change && this._change.revisions));
-          }
-          break;
-        case 188:  // ','
-          e.preventDefault();
-          this._openPrefs();
-          break;
+      e.preventDefault();
+      this.$.cursor.moveLeft();
+    },
+
+    _handleShiftRightKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this.$.cursor.moveRight();
+    },
+
+    _handleUpKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this.$.diff.displayLine = true;
+      this.$.cursor.moveUp();
+    },
+
+    _handleDownKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this.$.diff.displayLine = true;
+      this.$.cursor.moveDown();
+    },
+
+    _handleCKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+      if (this.$.diff.isRangeSelected()) { return; }
+
+      e.preventDefault();
+      var line = this.$.cursor.getTargetLineElement();
+      if (line) {
+        this.$.diff.addDraftAtLine(line);
       }
     },
 
+    _handleLeftBracketKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this._navToFile(this._path, this._fileList, -1);
+    },
+
+    _handleRightBracketKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this._navToFile(this._path, this._fileList, 1);
+    },
+
+    _handleNKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      if (e.detail.keyboardEvent.shiftKey) {
+        this.$.cursor.moveToNextCommentThread();
+      } else {
+        this.$.cursor.moveToNextChunk();
+      }
+    },
+
+    _handlePKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      if (e.detail.keyboardEvent.shiftKey) {
+        this.$.cursor.moveToPreviousCommentThread();
+      } else {
+        this.$.cursor.moveToPreviousChunk();
+      }
+    },
+
+    _handleAKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
+        e.preventDefault();
+        this.$.diff.toggleLeftDiff();
+        return;
+      }
+
+      if (!this._loggedIn) { return; }
+
+      this.set('changeViewState.showReplyDialog', true);
+      e.preventDefault();
+      this._navToChangeView();
+    },
+
+    _handleUKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this._navToChangeView();
+    },
+
+    _handleCommaKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+
+      e.preventDefault();
+      this._openPrefs();
+    },
+
+    _navToChangeView: function() {
+      if (!this._changeNum || !this._patchRange.patchNum) { return; }
+
+      page.show(this._getChangePath(
+          this._changeNum,
+          this._patchRange,
+          this._change && this._change.revisions));
+    },
+
     _navToFile: function(path, fileList, direction) {
       var url = this._computeNavLinkURL(path, fileList, direction);
       if (!url) { return; }
@@ -556,10 +598,6 @@
       history.replaceState(null, null, '#' + this.$.cursor.getAddress());
     },
 
-    _handleDropdownChange: function(e) {
-      e.target.blur();
-    },
-
     _computeDownloadLink: function(changeNum, patchRange, path) {
       var url = this.changeBaseURL(changeNum, patchRange.patchNum);
       url += '/patch?zip&path=' + encodeURIComponent(path);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index e423e45..c6ccb1b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -62,7 +62,7 @@
 
     test('toggle left diff with a hotkey', function() {
       var toggleLeftDiffStub = sandbox.stub(element.$.diff, 'toggleLeftDiff');
-      MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'a'
+      MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
       assert.isTrue(toggleLeftDiffStub.calledOnce);
     });
 
@@ -82,29 +82,29 @@
       element.changeViewState.selectedFileIndex = 1;
 
       var showStub = sandbox.stub(page, 'show');
-      MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'u'
+      MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
       assert(showStub.lastCall.calledWithExactly('/c/42/'),
           'Should navigate to /c/42/');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 221);  // ']'
+      MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
       assert(showStub.lastCall.calledWithExactly('/c/42/10/wheatley.md'),
           'Should navigate to /c/42/10/wheatley.md');
       element._path = 'wheatley.md';
       assert.equal(element.changeViewState.selectedFileIndex, 2);
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/10/glados.txt'),
           'Should navigate to /c/42/10/glados.txt');
       element._path = 'glados.txt';
       assert.equal(element.changeViewState.selectedFileIndex, 1);
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/10/chell.go'),
           'Should navigate to /c/42/10/chell.go');
       element._path = 'chell.go';
       assert.equal(element.changeViewState.selectedFileIndex, 0);
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/'),
           'Should navigate to /c/42/');
       assert.equal(element.changeViewState.selectedFileIndex, 0);
@@ -112,33 +112,33 @@
       var showPrefsStub = sandbox.stub(element.$.prefsOverlay, 'open',
           function() { return Promise.resolve({}); });
 
-      MockInteractions.pressAndReleaseKeyOn(element, 188);  // ','
+      MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
       assert(showPrefsStub.calledOnce);
 
       var scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk');
-      MockInteractions.pressAndReleaseKeyOn(element, 78);  // 'n'
+      MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
       assert(scrollStub.calledOnce);
 
       scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk');
-      MockInteractions.pressAndReleaseKeyOn(element, 80);  // 'p'
+      MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
       assert(scrollStub.calledOnce);
 
       scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread');
-      MockInteractions.pressAndReleaseKeyOn(element, 78, ['shift']);  // 'N'
+      MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
       assert(scrollStub.calledOnce);
 
       scrollStub = sandbox.stub(element.$.cursor,
           'moveToPreviousCommentThread');
-      MockInteractions.pressAndReleaseKeyOn(element, 80, ['shift']);  // 'P'
+      MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p');
       assert(scrollStub.calledOnce);
 
       var computeContainerClassStub = sandbox.stub(element.$.diff,
           '_computeContainerClass');
-      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+      MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
       assert(computeContainerClassStub.lastCall.calledWithExactly(
           false, 'SIDE_BY_SIDE', true));
 
-      MockInteractions.pressAndReleaseKeyOn(element, 27);  // 'escape'
+      MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
       assert(computeContainerClassStub.lastCall.calledWithExactly(
           false, 'SIDE_BY_SIDE', false));
     });
@@ -175,39 +175,39 @@
 
       var showStub = sandbox.stub(page, 'show');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
+      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' +
           'only work when the user is logged in.');
       assert.isNull(window.sessionStorage.getItem(
           'changeView.showReplyDialog'));
 
       element._loggedIn = true;
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
+      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       assert.isTrue(element.changeViewState.showReplyDialog);
 
       assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
           'Should navigate to /c/42/5..10');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'u'
+      MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
       assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
           'Should navigate to /c/42/5..10');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 221);  // ']'
+      MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
       assert(showStub.lastCall.calledWithExactly('/c/42/5..10/wheatley.md'),
           'Should navigate to /c/42/5..10/wheatley.md');
       element._path = 'wheatley.md';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/5..10/glados.txt'),
           'Should navigate to /c/42/5..10/glados.txt');
       element._path = 'glados.txt';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/5..10/chell.go'),
           'Should navigate to /c/42/5..10/chell.go');
       element._path = 'chell.go';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/5..10'),
           'Should navigate to /c/42/5..10');
     });
@@ -229,39 +229,39 @@
 
       var showStub = sandbox.stub(page, 'show');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
+      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' +
           'only work when the user is logged in.');
       assert.isNull(window.sessionStorage.getItem(
           'changeView.showReplyDialog'));
 
       element._loggedIn = true;
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
+      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       assert.isTrue(element.changeViewState.showReplyDialog);
 
       assert(showStub.lastCall.calledWithExactly('/c/42/1'),
           'Should navigate to /c/42/1');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'u'
+      MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
       assert(showStub.lastCall.calledWithExactly('/c/42/1'),
           'Should navigate to /c/42/1');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 221);  // ']'
+      MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
       assert(showStub.lastCall.calledWithExactly('/c/42/1/wheatley.md'),
           'Should navigate to /c/42/1/wheatley.md');
       element._path = 'wheatley.md';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/1/glados.txt'),
           'Should navigate to /c/42/1/glados.txt');
       element._path = 'glados.txt';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/1/chell.go'),
           'Should navigate to /c/42/1/chell.go');
       element._path = 'chell.go';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/1'),
           'Should navigate to /c/42/1');
     });
@@ -278,39 +278,39 @@
 
       var showStub = sandbox.stub(page, 'show');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
+      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' +
           'only work when the user is logged in.');
       assert.isNull(window.sessionStorage.getItem(
           'changeView.showReplyDialog'));
 
       element._loggedIn = true;
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
+      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
       assert.isTrue(element.changeViewState.showReplyDialog);
 
       assert(showStub.lastCall.calledWithExactly('/c/42/1'),
           'Should navigate to /c/42/1');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'u'
+      MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
       assert(showStub.lastCall.calledWithExactly('/c/42/1'),
           'Should navigate to /c/42/1');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 221);  // ']'
+      MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
       assert(showStub.lastCall.calledWithExactly('/c/42/1/wheatley.md'),
           'Should navigate to /c/42/1/wheatley.md');
       element._path = 'wheatley.md';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/1/glados.txt'),
           'Should navigate to /c/42/1/glados.txt');
       element._path = 'glados.txt';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/1/chell.go'),
           'Should navigate to /c/42/1/chell.go');
       element._path = 'chell.go';
 
-      MockInteractions.pressAndReleaseKeyOn(element, 219);  // '['
+      MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
       assert(showStub.lastCall.calledWithExactly('/c/42/1'),
           'Should navigate to /c/42/1');
     });
@@ -457,7 +457,6 @@
     test('diff mode selector correctly toggles the diff', function() {
       var select = element.$.modeSelect;
       var diffDisplay = element.$.diff;
-      var blurSpy = sandbox.spy(select, 'blur');
       element._userPrefs = {diff_view: 'SIDE_BY_SIDE'};
 
       // The mode selected in the view state reflects the selected option.
@@ -477,7 +476,6 @@
       assert.equal(element._getDiffViewMode(), newMode);
       assert.equal(element._getDiffViewMode(), select.value);
       assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
-      assert(blurSpy.called, 'select should be blurred after selection');
     });
 
     test('diff mode selector initializes from preferences', function() {
@@ -557,14 +555,6 @@
       assert.equal(element.$.cursor.side, 'left');
     });
 
-    test('_checkForModifiers', function() {
-      assert.isTrue(element._checkForModifiers({altKey: true}));
-      assert.isTrue(element._checkForModifiers({ctrlKey: true}));
-      assert.isTrue(element._checkForModifiers({metaKey: true}));
-      assert.isTrue(element._checkForModifiers({shiftKey: true}));
-      assert.isFalse(element._checkForModifiers({}));
-    });
-
     test('_shortenPath with long path should add ellipsis', function() {
       var path =
           'level1/level2/level3/level4/file.js';
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index 568b5e0..c610073 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -51,6 +51,10 @@
       'mousedown': '_handleMouseDown', // See https://crbug.com/gerrit/4767
     },
 
+    keyBindings: {
+      'c': '_handleCKey',
+    },
+
     placeAbove: function(el) {
       var rect = this._getTargetBoundingRect(el);
       var boxRect = this.getBoundingClientRect();
@@ -74,17 +78,11 @@
       return rect;
     },
 
-    _checkForModifiers: function(e) {
-      return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || false;
-    },
-
-    _handleKey: function(e) {
+    _handleCKey: function(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-      if (e.keyCode === 67) { // 'c'
-        if (this._checkForModifiers(e)) { return; }
-        e.preventDefault();
-        this._fireCreateComment();
-      }
+
+      e.preventDefault();
+      this._fireCreateComment();
     },
 
     _handleMouseDown: function(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index c12966d..79ff2a5 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -49,12 +49,12 @@
     });
 
     test('ignores regular keys', function() {
-      MockInteractions.pressAndReleaseKeyOn(document.body, 27); // 'esc'
+      MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc');
       assert.isFalse(element.fire.called);
     });
 
     test('reacts to hotkey', function() {
-      MockInteractions.pressAndReleaseKeyOn(document.body, 67); // 'c'
+      MockInteractions.pressAndReleaseKeyOn(document.body, 67, null, 'c');
       assert.isTrue(element.fire.called);
     });
 
@@ -68,7 +68,7 @@
       };
       element.side = 'left';
       element.range = range;
-      MockInteractions.pressAndReleaseKeyOn(document.body, 67); // 'c'
+      MockInteractions.pressAndReleaseKeyOn(document.body, 67, null, 'c');
       assert(element.fire.calledWithExactly(
           'create-comment',
           {
@@ -117,13 +117,5 @@
         document.createRange.restore();
       });
     });
-
-    test('_checkForModifiers', function() {
-      assert.isTrue(element._checkForModifiers({altKey: true}));
-      assert.isTrue(element._checkForModifiers({ctrlKey: true}));
-      assert.isTrue(element._checkForModifiers({metaKey: true}));
-      assert.isTrue(element._checkForModifiers({shiftKey: true}));
-      assert.isFalse(element._checkForModifiers({}));
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index b758ff0..1ef6c2d 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -62,6 +62,10 @@
       Gerrit.KeyboardShortcutBehavior,
     ],
 
+    keyBindings: {
+      '?': '_showKeyboardShortcuts',
+    },
+
     attached: function() {
       this.$.restAPI.getAccount().then(function(account) {
         this._account = account;
@@ -194,12 +198,9 @@
       }
     },
 
-    _handleKey: function(e) {
+    _showKeyboardShortcuts(e) {
       if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-
-      if (e.keyCode === 191 && e.shiftKey) {  // '/' or '?' with shift key.
-        this.$.keyboardShortcuts.open();
-      }
+      this.$.keyboardShortcuts.open();
     },
 
     _handleKeyboardShortcutDialogClose: function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index 864114f..5de54bb 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 1c9e010..0626288 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -140,6 +140,10 @@
       this.$.input.focus();
     },
 
+    selectAll: function() {
+      this.$.input.setSelectionRange(0, this.$.input.value.length);
+    },
+
     clear: function() {
       this.text = '';
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 03fa8e43..394b2c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -94,7 +94,7 @@
         var cancelHandler = sinon.spy();
         element.addEventListener('cancel', cancelHandler);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.input, 27); // Esc
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
         assert.isTrue(cancelHandler.called);
         assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
 
@@ -128,19 +128,22 @@
 
         assert.equal(element.$.cursor.index, 0);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
+            'down');
 
         assert.equal(element.$.cursor.index, 1);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null,
+            'down');
 
         assert.equal(element.$.cursor.index, 2);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.input, 38); // Up
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up');
 
         assert.equal(element.$.cursor.index, 1);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+            'enter');
 
         assert.equal(element.value, 1);
         assert.isTrue(commitHandler.called);
@@ -163,7 +166,8 @@
         var commitHandler = sinon.spy();
         element.addEventListener('commit', commitHandler);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+            'enter');
 
         assert.isTrue(commitHandler.called);
         assert.equal(element.text, 'suggestion');
@@ -184,7 +188,8 @@
         var commitHandler = sinon.spy();
         element.addEventListener('commit', commitHandler);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+            'enter');
 
         assert.isTrue(commitHandler.called);
         assert.equal(element.text, '');
@@ -234,7 +239,8 @@
         var commitHandler = sinon.spy();
         element.addEventListener('commit', commitHandler);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
+        MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
+            'enter');
 
         assert.isTrue(commitHandler.called);
         assert.equal(element.text, 'blah 0');
@@ -245,10 +251,10 @@
     test('tab key completes only when suggestions exist', function() {
       var commitStub = sinon.stub(element, '_commit');
       element._suggestions = [];
-      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
       assert.isFalse(commitStub.called);
       element._suggestions = ['tunnel snakes rule!'];
-      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
       assert.isTrue(commitStub.called);
       commitStub.restore();
     });
@@ -258,11 +264,11 @@
       element.addEventListener('commit', commitHandler);
       element._suggestions = ['tunnel snakes rule!'];
       element.tabCompleteWithoutCommit = true;
-      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
       assert.isFalse(commitHandler.called);
       element.tabCompleteWithoutCommit = false;
       element._suggestions = ['tunnel snakes rule!'];
-      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
       assert.isTrue(commitHandler.called);
     });
 
@@ -301,7 +307,7 @@
     test('input-keydown event fired', function() {
       var listener = sinon.spy();
       element.addEventListener('input-keydown', listener);
-      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
       flushAsynchronousOperations();
       assert.isTrue(listener.called);
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index e109896..01d2585 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -39,6 +39,10 @@
       tabindex: '0',
     },
 
+    keyBindings: {
+      'space enter': '_handleCommitKey',
+    },
+
     _disabledChanged: function(disabled) {
       if (disabled) {
         this._enabledTabindex = this.getAttribute('tabindex');
@@ -46,13 +50,9 @@
       this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex);
     },
 
-    _handleKey: function(e) {
-      switch (e.keyCode) {
-        case 32:  // 'spacebar'
-        case 13:  // 'enter'
-          e.preventDefault();
-          this.click();
-      }
+    _handleCommitKey: function(e) {
+      e.preventDefault();
+      this.click();
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
index 817d8c5..9aa80b5 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -16,7 +16,6 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-overlay-behavior/iron-overlay-behavior.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 
 <dom-module id="gr-overlay">
   <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index f4a389a..9f271ed 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -24,28 +24,13 @@
       Polymer.IronOverlayBehavior,
     ],
 
-    detached: function() {
-      Gerrit.KeyboardShortcutBehavior.enable(this._id());
-    },
-
     open: function() {
       return new Promise(function(resolve) {
-        Gerrit.KeyboardShortcutBehavior.disable(this._id());
         Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments);
         this._awaitOpen(resolve);
       }.bind(this));
     },
 
-    close: function() {
-      Gerrit.KeyboardShortcutBehavior.enable(this._id());
-      Polymer.IronOverlayBehaviorImpl.close.apply(this, arguments);
-    },
-
-    cancel: function() {
-      Gerrit.KeyboardShortcutBehavior.enable(this._id());
-      Polymer.IronOverlayBehaviorImpl.cancel.apply(this, arguments);
-    },
-
     /**
      * Override the focus stops that iron-overlay-behavior tries to find.
      */