Merge changes from topic 'registration'

* changes:
  Fix misspelling of _shouldSuppressError
  Add namespacing for keyboard shortcut disabling
  Add account registration dialog to PolyGerrit
  Allow anything after "/register/"
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 18445b3..842c575 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -92,7 +92,7 @@
     serve("/starred").with(query("is:starred"));
 
     serveRegex("^/settings/?$").with(screen(PageLinks.SETTINGS));
-    serveRegex("^/register/?$").with(screen(PageLinks.REGISTER + "/"));
+    serveRegex("^/register(/.*)?$").with(registerScreen());
     serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
     serveRegex("^/p/(.*)$").with(queryProjectNew());
     serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
@@ -243,6 +243,18 @@
     return srv;
   }
 
+  private Key<HttpServlet> registerScreen() {
+    return key(new HttpServlet() {
+      private static final long serialVersionUID = 1L;
+
+      @Override
+      protected void doGet(final HttpServletRequest req,
+          final HttpServletResponse rsp) throws IOException {
+        toGerrit("/register" + req.getPathInfo(), req, rsp);
+      }
+    });
+  }
+
   static void toGerrit(final String target, final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     final StringBuilder url = new StringBuilder();
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
index 17acac8..f636650 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html
@@ -20,7 +20,9 @@
 
   /** @polymerBehavior Gerrit.KeyboardShortcutBehavior */
   var KeyboardShortcutBehavior = {
-    enabled: true,
+    // Set of identifiers currently blocking keyboard shortcuts. Stored as
+    // a map of string to the value of true.
+    _disablers: {},
 
     properties: {
       keyEventTarget: {
@@ -43,8 +45,12 @@
       this.keyEventTarget.removeEventListener('keydown', this._boundKeyHandler);
     },
 
-    shouldSupressKeyboardShortcut: function(e) {
-      if (!KeyboardShortcutBehavior.enabled) { return true; }
+    shouldSuppressKeyboardShortcut: function(e) {
+      for (var c in KeyboardShortcutBehavior._disablers) {
+        if (KeyboardShortcutBehavior._disablers[c] === true) {
+          return true;
+        }
+      }
       var getModifierState = e.getModifierState ?
           e.getModifierState.bind(e) :
           function() { return false; };
@@ -60,6 +66,14 @@
              target.tagName == 'A' ||
              target.tagName == 'GR-BUTTON';
     },
+
+    disable: function(id) {
+      KeyboardShortcutBehavior._disablers[id] = true;
+    },
+
+    enable: function(id) {
+      delete KeyboardShortcutBehavior._disablers[id];
+    },
   };
 
   window.Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior_test.html
new file mode 100644
index 0000000..5ec4145
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior_test.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>keyboard-shortcut-behavior</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="keyboard-shortcut-behavior.html">
+
+<test-fixture id="basic">
+  <template>
+    <test-element></test-element>
+  </template>
+</test-fixture>
+
+<script>
+  suite('keyboard-shortcut-behavior tests', function() {
+    var element;
+
+    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 []; },
+          },
+        },
+
+        _handleKey: function(e) {
+          if (!this.shouldSuppressKeyboardShortcut(e)) {
+            this.log.push(e.keyCode);
+          }
+        },
+      });
+    });
+
+    setup(function() {
+      element = fixture('basic');
+    });
+
+    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]);
+    });
+  });
+</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 4e17253..2be0afc 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
@@ -150,7 +150,7 @@
     },
 
     _handleKey: function(e) {
-      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
 
       if (this.groups == null) { return; }
       var len = 0;
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 3d1cac7e..c794a4e 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
@@ -585,7 +585,7 @@
     },
 
     _handleKey: function(e) {
-      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
       switch (e.keyCode) {
         case 65:  // 'a'
           if (this._loggedIn && !e.shiftKey) {
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 478faf5..22c82a0 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
@@ -295,7 +295,7 @@
     },
 
     _handleKey: function(e) {
-      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
       switch (e.keyCode) {
         case 37: // left
           if (e.shiftKey && this._showInlineDiffs) {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index a38152a..479f389 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -39,7 +39,7 @@
       this.unlisten(document, 'network-error', '_handleNetworkError');
     },
 
-    _shouldSupressError: function(msg) {
+    _shouldSuppressError: function(msg) {
       return msg.indexOf(TOO_MANY_FILES) > -1;
     },
 
@@ -54,7 +54,7 @@
         }.bind(this));
       } else {
         e.detail.response.text().then(function(text) {
-          if (!this._shouldSupressError(text)) {
+          if (!this._shouldSuppressError(text)) {
             this._showAlert('Server error: ' + text);
           }
         }.bind(this));
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 6fc3cc1..2f017de 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -79,6 +79,10 @@
       this.unlisten(window, 'location-change', '_handleLocationChange');
     },
 
+    reload: function() {
+      this._loadAccount();
+    },
+
     _handleLocationChange: function(e) {
       this._loginURL = '/login/' + encodeURIComponent(
           window.location.pathname +
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 05b191c..85e72c0 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -62,7 +62,7 @@
         // In certain login flows the server may redirect to a hash without
         // a leading slash, which page.js doesn't handle correctly.
         if (data.hash[0] !== '/') {
-          data.hash = '/' + data.hash
+          data.hash = '/' + data.hash;
         }
         page.redirect(data.hash);
         return;
@@ -181,6 +181,12 @@
       });
     });
 
+    page(/^\/register(\/.*)?/, function(ctx) {
+      app.params = {justRegistered: true};
+      var path = ctx.params[0] || '/';
+      page.show(path);
+    });
+
     page.start();
   });
 })();
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 2818121..4fd1e19 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
@@ -293,7 +293,7 @@
     },
 
     _handleKey: function(e) {
-      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
       switch (e.keyCode) {
         case 191:  // '/' or '?' with shift key.
           // TODO(andybons): Localization using e.key/keypress event.
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 963084a..90a4be1 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
@@ -89,7 +89,7 @@
     },
 
     _handleKey: function(e) {
-      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
       if (e.keyCode === 69) { // 'e'
         e.preventDefault();
         this._expandCollapseComments(e.shiftKey);
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 c5779a4..3c7fe02 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
@@ -189,7 +189,7 @@
     },
 
     _handleKey: function(e) {
-      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
 
       switch (e.keyCode) {
         case 37: // left
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 29b3c19..568b5e0 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
@@ -79,7 +79,7 @@
     },
 
     _handleKey: function(e) {
-      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
       if (e.keyCode === 67) { // 'c'
         if (this._checkForModifiers(e)) { return; }
         e.preventDefault();
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 633e8f2..a403e6c 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -28,6 +28,7 @@
 <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
 <link rel="import" href="./change/gr-change-view/gr-change-view.html">
 <link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
+<link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
 <link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
 
 <link rel="import" href="./shared/gr-overlay/gr-overlay.html">
@@ -86,7 +87,8 @@
         color: #b71c1c;
       }
     </style>
-    <gr-main-header search-query="{{params.query}}"></gr-main-header>
+    <gr-main-header id="mainHeader" search-query="{{params.query}}">
+    </gr-main-header>
     <main>
       <template is="dom-if" if="[[_showChangeListView]]" restamp="true">
         <gr-change-list-view
@@ -113,7 +115,9 @@
             change-view-state="{{_viewState.changeView}}"></gr-diff-view>
       </template>
       <template is="dom-if" if="[[_showSettingsView]]" restamp="true">
-        <gr-settings-view></gr-settings-view>
+        <gr-settings-view
+            on-account-detail-update="_handleAccountDetailUpdate">
+        </gr-settings-view>
       </template>
       <div id="errorView" class="errorView" hidden>
         <div class="errorEmoji">[[_lastError.emoji]]</div>
@@ -138,6 +142,12 @@
           view="[[params.view]]"
           on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog>
     </gr-overlay>
+    <gr-overlay id="registration" with-backdrop>
+      <gr-registration-dialog
+          on-account-detail-update="_handleAccountDetailUpdate"
+          on-close="_handleRegistrationDialogClose">
+      </gr-registration-dialog>
+    </gr-overlay>
     <gr-error-manager></gr-error-manager>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-reporting id="reporting"></gr-reporting>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 32130f8..b758ff0 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -108,6 +108,9 @@
       this.set('_showChangeView', view === 'gr-change-view');
       this.set('_showDiffView', view === 'gr-diff-view');
       this.set('_showSettingsView', view === 'gr-settings-view');
+      if (this.params.justRegistered) {
+        this.$.registration.open();
+      }
     },
 
     _loadPlugins: function(plugins) {
@@ -192,7 +195,7 @@
     },
 
     _handleKey: function(e) {
-      if (this.shouldSupressKeyboardShortcut(e)) { return; }
+      if (this.shouldSuppressKeyboardShortcut(e)) { return; }
 
       if (e.keyCode === 191 && e.shiftKey) {  // '/' or '?' with shift key.
         this.$.keyboardShortcuts.open();
@@ -202,5 +205,17 @@
     _handleKeyboardShortcutDialogClose: function() {
       this.$.keyboardShortcuts.close();
     },
+
+    _handleAccountDetailUpdate: function(e) {
+      this.$.mainHeader.reload();
+      if (this.params.view === 'gr-settings-view') {
+        this.$$('gr-settings-view').reloadAccountDetail();
+      }
+    },
+
+    _handleRegistrationDialogClose: function(e) {
+      this.params.justRegistered = false;
+      this.$.registration.close();
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 3930a78..2704ce5 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -17,6 +17,12 @@
   Polymer({
     is: 'gr-account-info',
 
+    /**
+     * Fired when account details are changed.
+     *
+     * @event account-detail-update
+     */
+
     properties: {
       mutable: {
         type: Boolean,
@@ -72,6 +78,7 @@
       return this.$.restAPI.setAccountName(this._account.name).then(function() {
         this.hasUnsavedChanges = false;
         this._saving = false;
+        this.fire('account-detail-update');
       }.bind(this));
     },
 
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
new file mode 100644
index 0000000..ee358d5
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -0,0 +1,98 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/gr-settings-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-registration-dialog">
+  <template>
+    <style include="gr-settings-styles"></style>
+    <style>
+      :host {
+        display: block;
+      }
+      main {
+        max-width: 46em;
+      }
+      hr {
+        margin-top: 1em;
+        margin-bottom: 1em;
+      }
+      header {
+        border-bottom: 1px solid #ddd;
+        font-weight: bold;
+      }
+      header,
+      main,
+      footer {
+        padding: .5em .65em;
+      }
+      footer {
+        display: flex;
+        justify-content: space-between;
+      }
+    </style>
+    <main class="gr-settings-styles">
+      <header>Please confirm your contact information</header>
+      <main>
+        <p>
+          The following contact information was automatically obtained when you
+          signed in to the site. This information is used to display who you are
+          to others, and to send updates to code reviews you have either started
+          or subscribed to.
+        </p>
+        <hr>
+        <section>
+          <div class="title">Full Name</div>
+          <input
+              is="iron-input"
+              id="name"
+              bind-value="{{_account.name}}"
+              disabled="[[_saving]]"
+              on-keydown="_handleNameKeydown">
+        </section>
+        <section>
+          <div class="title">Preferred Email</div>
+          <select
+              is="gr-select"
+              id="email"
+              bind-value="{{_account.email}}"
+              disabled="[[_saving]]">
+            <option value="[[_account.email]]">[[_account.email]]</option>
+            <template is="dom-repeat" items="[[_account.secondary_emails]]">
+              <option value="[[item]]">[[item]]</option>
+            </template>
+          </select>
+        </section>
+      </main>
+      <footer>
+        <gr-button
+            id="saveButton"
+            primary
+            disabled="[[_saving]]"
+            on-tap="_handleSave">Save</gr-button>
+        <gr-button
+            id="closeButton"
+            disabled="[[_saving]]"
+            on-tap="_handleClose">Close</gr-button>
+      </footer>
+    </main>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-registration-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
new file mode 100644
index 0000000..9acdba9
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -0,0 +1,79 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-registration-dialog',
+
+    /**
+     * Fired when account details are changed.
+     *
+     * @event account-detail-update
+     */
+
+    /**
+     * Fired when the close button is pressed.
+     *
+     * @event close
+     */
+
+    properties: {
+      _account: Object,
+      _saving: Boolean,
+    },
+
+    hostAttributes: {
+      role: 'dialog',
+    },
+
+    attached: function() {
+      this.$.restAPI.getAccount().then(function(account) {
+        this._account = account;
+      }.bind(this));
+    },
+
+    _handleNameKeydown: function(e) {
+      if (e.keyCode === 13) { // Enter
+        e.stopPropagation();
+        this._save();
+      }
+    },
+
+    _save: function() {
+      this._saving = true;
+      var promises = [
+        this.$.restAPI.setAccountName(this.$.name.value),
+        this.$.restAPI.setPreferredAccountEmail(this.$.email.value),
+      ];
+      return Promise.all(promises).then(function() {
+        this._saving = false;
+        this.fire('account-detail-update');
+      }.bind(this));
+    },
+
+    _handleSave: function(e) {
+      e.preventDefault();
+      this._save().then(function() {
+        this.fire('close');
+      }.bind(this));
+    },
+
+    _handleClose: function(e) {
+      e.preventDefault();
+      this._saving = true; // disable buttons indefinitely
+      this.fire('close');
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
new file mode 100644
index 0000000..0b9dc9c
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-registration-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-registration-dialog.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-registration-dialog></gr-registration-dialog>
+  </template>
+</test-fixture>
+
+<test-fixture id="blank">
+  <template>
+    <div></div>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-registration-dialog tests', function() {
+    var element;
+    var account;
+    var _listeners;
+
+    setup(function(done) {
+      _listeners = {};
+
+      account = {
+        name: 'name',
+        email: 'email',
+        secondary_emails: [
+          'email2',
+          'email3',
+        ],
+      };
+
+      stub('gr-rest-api-interface', {
+        getAccount: function() {
+          // Once the account is resolved, we can let the test proceed.
+          flush(done);
+          return Promise.resolve(account);
+        },
+        setAccountName: function(name) {
+          account.name = name;
+          return Promise.resolve();
+        },
+        setPreferredAccountEmail: function(email) {
+          account.email = email;
+          return Promise.resolve();
+        },
+      });
+
+      element = fixture('basic');
+    });
+
+    teardown(function() {
+      for (var eventType in _listeners) {
+        if (_listeners.hasOwnProperty(eventType)) {
+          element.removeEventListener(eventType, _listeners[eventType]);
+        }
+      }
+    });
+
+    function listen(eventType) {
+      return new Promise(function(resolve) {
+        _listeners[eventType] = function() { resolve(); };
+        element.addEventListener(eventType, _listeners[eventType]);
+      });
+    }
+
+    function save(opt_action) {
+      var promise = listen('account-detail-update');
+      if (opt_action) {
+        opt_action();
+      } else {
+        MockInteractions.tap(element.$.saveButton);
+      }
+      return promise;
+    }
+
+    function close(opt_action) {
+      var promise = listen('close');
+      if (opt_action) {
+        opt_action();
+      } else {
+        MockInteractions.tap(element.$.closeButton);
+      }
+      return promise;
+    }
+
+    test('fires the close event on close', function(done) {
+      close().then(done);
+    });
+
+    test('fires the close event on save', function(done) {
+      close(function() {
+        MockInteractions.tap(element.$.saveButton);
+      }).then(done);
+    });
+
+    test('saves name and preferred email', function(done) {
+      element.$.name.value = 'new name';
+      element.$.email.value = 'email3';
+
+      // Nothing should be committed yet.
+      assert.equal(account.name, 'name');
+      assert.equal(account.email, 'email');
+
+      // Save and verify new values are committed.
+      save().then(function() {
+        assert.equal(account.name, 'new name');
+        assert.equal(account.email, 'email3');
+      }).then(done);
+    });
+
+    test('pressing enter saves name', function(done) {
+      element.$.name.value = 'entered name';
+      save(function() {
+        MockInteractions.pressAndReleaseKeyOn(element.$.name, 13);  // 'enter'
+      }).then(function() {
+        assert.equal(account.name, 'entered name');
+      }).then(done);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 6c62408..81a56a1 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -134,6 +134,13 @@
       this.unlisten(window, 'scroll', '_handleBodyScroll');
     },
 
+    reloadAccountDetail: function() {
+      Promise.all([
+        this.$.accountInfo.loadData(),
+        this.$.emailEditor.loadData(),
+      ]);
+    },
+
     _handleBodyScroll: function(e) {
       if (this._headerHeight === undefined) {
         var top = this.$.settingsNav.offsetTop;
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 da28e49..f4a389a 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -25,25 +25,24 @@
     ],
 
     detached: function() {
-      // For good measure.
-      Gerrit.KeyboardShortcutBehavior.enabled = true;
+      Gerrit.KeyboardShortcutBehavior.enable(this._id());
     },
 
     open: function() {
       return new Promise(function(resolve) {
-        Gerrit.KeyboardShortcutBehavior.enabled = false;
+        Gerrit.KeyboardShortcutBehavior.disable(this._id());
         Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments);
         this._awaitOpen(resolve);
       }.bind(this));
     },
 
     close: function() {
-      Gerrit.KeyboardShortcutBehavior.enabled = true;
+      Gerrit.KeyboardShortcutBehavior.enable(this._id());
       Polymer.IronOverlayBehaviorImpl.close.apply(this, arguments);
     },
 
     cancel: function() {
-      Gerrit.KeyboardShortcutBehavior.enabled = true;
+      Gerrit.KeyboardShortcutBehavior.enable(this._id());
       Polymer.IronOverlayBehaviorImpl.cancel.apply(this, arguments);
     },
 
@@ -72,5 +71,9 @@
       }.bind(this);
       step.call(this);
     },
+
+    _id: function() {
+      return this.getAttribute('id') || 'global';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index f46404a..91bde6b 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -239,12 +239,39 @@
 
     setPreferredAccountEmail: function(email, opt_errFn, opt_ctx) {
       return this.send('PUT', '/accounts/self/emails/' +
-          encodeURIComponent(email) + '/preferred', null, opt_errFn, opt_ctx);
+          encodeURIComponent(email) + '/preferred', null,
+          opt_errFn, opt_ctx).then(function() {
+        // If result of getAccountEmails is in cache, update it in the cache
+        // so we don't have to invalidate it.
+        var cachedEmails = this._cache['/accounts/self/emails'];
+        if (cachedEmails) {
+          var emails = cachedEmails.map(function(entry) {
+            if (entry.email === email) {
+              return {email: email, preferred: true};
+            } else {
+              return {email: email};
+            }
+          });
+          this._cache['/accounts/self/emails'] = emails;
+        }
+      }.bind(this));
     },
 
     setAccountName: function(name, opt_errFn, opt_ctx) {
       return this.send('PUT', '/accounts/self/name', {name: name}, opt_errFn,
-          opt_ctx);
+          opt_ctx).then(function(response) {
+        // If result of getAccount is in cache, update it in the cache
+        // so we don't have to invalidate it.
+        var cachedAccount = this._cache['/accounts/self/detail'];
+        if (cachedAccount) {
+          return this.getResponseObject(response).then(function(newName) {
+            // Replace object in cache with new object to force UI updates.
+            // TODO(logan): Polyfill for Object.assign in IE
+            this._cache['/accounts/self/detail'] = Object.assign(
+                {}, cachedAccount, {name: newName});
+          }.bind(this));
+        }
+      }.bind(this));
     },
 
     getAccountGroups: function() {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index c5df0a2..a25656f 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -74,6 +74,7 @@
     'settings/gr-group-list/gr-group-list_test.html',
     'settings/gr-http-password/gr-http-password_test.html',
     'settings/gr-menu-editor/gr-menu-editor_test.html',
+    'settings/gr-registration-dialog/gr-registration-dialog_test.html',
     'settings/gr-settings-view/gr-settings-view_test.html',
     'settings/gr-ssh-editor/gr-ssh-editor_test.html',
     'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',