Bug 819428 - Re-jig keyboard navigation for the Downloads Panel. r=mak.
authorMike Conley <mconley@mozilla.com>
Mon, 14 Jan 2013 12:59:40 -0500
changeset 118781 db7914a84af32f98f212a6c2d48dc9b312b717e0
parent 118780 ab8bfaeaeafc6f271b943a89c7b7da94397c6521
child 118782 620f2cbe08bebb9baa29caf52a09572dae4db7be
push id24180
push useremorley@mozilla.com
push dateTue, 15 Jan 2013 22:58:27 +0000
treeherdermozilla-central@72e34ce7fd92 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs819428
milestone21.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 819428 - Re-jig keyboard navigation for the Downloads Panel. r=mak.
browser/components/downloads/content/downloads.css
browser/components/downloads/content/downloads.js
browser/components/downloads/content/downloadsOverlay.xul
browser/themes/gnomestripe/downloads/downloads.css
browser/themes/pinstripe/downloads/downloads.css
browser/themes/winstripe/downloads/downloads-aero.css
browser/themes/winstripe/downloads/downloads.css
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -1,18 +1,12 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/*** Download panel ***/
-
-#downloadsPanel {
-  -moz-user-focus: normal;
-}
-
 /*** Download items ***/
 
 richlistitem[type="download"] {
   -moz-binding: url('chrome://browser/content/downloads/download.xml#download');
 }
 
 richlistitem[type="download"]:not([selected]) button {
   /* Only focus buttons in the selected item. */
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -30,16 +30,43 @@
  * widget, in particular the "commands" that apply to multiple items, and
  * dispatches the commands that apply to individual items.
  *
  * DownloadsViewItemController
  * Handles all the user interaction events, in particular the "commands",
  * related to a single item in the downloads list widgets.
  */
 
+/**
+ * A few words on focus and focusrings
+ *
+ * We do quite a few hacks in the Downloads Panel for focusrings. In fact, we
+ * basically suppress most if not all XUL-level focusrings, and style/draw
+ * them ourselves (using :focus instead of -moz-focusring). There are a few
+ * reasons for this:
+ *
+ * 1) Richlists on OSX don't have focusrings; instead, they are shown as
+ *    selected. This makes for some ambiguity when we have a focused/selected
+ *    item in the list, and the mouse is hovering a completed download (which
+ *    highlights).
+ * 2) Windows doesn't show focusrings until after the first time that tab is
+ *    pressed (and by then you're focusing the second item in the panel).
+ * 3) Richlistbox sets -moz-focusring even when we select it with a mouse.
+ *
+ * In general, the desired behaviour is to focus the first item after pressing
+ * tab/down, and show that focus with a ring. Then, if the mouse moves over
+ * the panel, to hide that focus ring; essentially resetting us to the state
+ * before pressing the key.
+ *
+ * We end up capturing the tab/down key events, and preventing their default
+ * behaviour. We then set a "keyfocus" attribute on the panel, which allows
+ * us to draw a ring around the currently focused element. If the panel is
+ * closed or the mouse moves over the panel, we remove the attribute.
+ */
+
 "use strict";
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
                                   "resource://gre/modules/DownloadUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
@@ -203,16 +230,52 @@ const DownloadsPanel = {
    */
   get isPanelShowing()
   {
     return this._state == this.kStateWaitingData ||
            this._state == this.kStateWaitingAnchor ||
            this._state == this.kStateShown;
   },
 
+  /**
+   * Returns whether the user has started keyboard navigation.
+   */
+  get keyFocusing()
+  {
+    return this.panel.hasAttribute("keyfocus");
+  },
+
+  /**
+   * Set to true if the user has started keyboard navigation, and we should be
+   * showing focusrings in the panel. Also adds a mousemove event handler to
+   * the panel which disables keyFocusing.
+   */
+  set keyFocusing(aValue)
+  {
+    if (aValue) {
+      this.panel.setAttribute("keyfocus", "true");
+      this.panel.addEventListener("mousemove", this);
+    } else {
+      this.panel.removeAttribute("keyfocus");
+      this.panel.removeEventListener("mousemove", this);
+    }
+    return aValue;
+  },
+
+  /**
+   * Handles the mousemove event for the panel, which disables focusring
+   * visualization.
+   */
+  handleEvent: function DP_handleEvent(aEvent)
+  {
+    if (aEvent.type == "mousemove") {
+      this.keyFocusing = false;
+    }
+  },
+
   //////////////////////////////////////////////////////////////////////////////
   //// Callback functions from DownloadsView
 
   /**
    * Called after data loading finished.
    */
   onViewLoadCompleted: function DP_onViewLoadCompleted()
   {
@@ -251,16 +314,20 @@ const DownloadsPanel = {
 
   onPopupHidden: function DP_onPopupHidden(aEvent)
   {
     // Ignore events raised by nested popups.
     if (aEvent.target != aEvent.currentTarget) {
       return;
     }
 
+    // Removes the keyfocus attribute so that we stop handling keyboard
+    // navigation.
+    this.keyFocusing = false;
+
     // Since at most one popup is open at any given time, we can set globally.
     DownloadsCommon.getIndicatorData(window).attentionSuppressed = false;
 
     // Allow the anchor to be hidden.
     DownloadsButton.releaseAnchor();
 
     // Allow the panel to be reopened.
     this._state = this.kStateHidden;
@@ -286,35 +353,92 @@ const DownloadsPanel = {
 
   /**
    * Attach event listeners to a panel element. These listeners should be
    * removed in _unattachEventListeners. This is called automatically after the
    * panel has successfully loaded.
    */
   _attachEventListeners: function DP__attachEventListeners()
   {
+    // Handle keydown to support accel-V.
     this.panel.addEventListener("keydown", this._onKeyDown.bind(this), false);
+    // Handle keypress to be able to preventDefault() events before they reach
+    // the richlistbox, for keyboard navigation.
+    this.panel.addEventListener("keypress", this._onKeyPress.bind(this), false);
   },
 
   /**
    * Unattach event listeners that were added in _attachEventListeners. This
    * is called automatically on panel termination.
    */
   _unattachEventListeners: function DP__unattachEventListeners()
   {
     this.panel.removeEventListener("keydown", this._onKeyDown.bind(this),
                                    false);
+    this.panel.removeEventListener("keypress", this._onKeyPress.bind(this),
+                                   false);
+  },
+
+  _onKeyPress: function DP__onKeyPress(aEvent)
+  {
+    // Handle unmodified keys only.
+    if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
+      return;
+    }
+
+    let richListBox = DownloadsView.richListBox;
+
+    // If the user has pressed the tab, up, or down cursor key, start keyboard
+    // navigation, thus enabling focusrings in the panel.  Keyboard navigation
+    // is automatically disabled if the user moves the mouse on the panel, or
+    // if the panel is closed.
+    if ((aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_TAB ||
+        aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP ||
+        aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) &&
+        !this.keyFocusing) {
+      this.keyFocusing = true;
+      aEvent.preventDefault();
+      return;
+    }
+
+    if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
+      // If the last element in the list is selected, or the footer is already
+      // focused, focus the footer.
+      if (richListBox.selectedItem === richListBox.lastChild ||
+          document.activeElement.parentNode.id === "downloadsFooter") {
+        DownloadsFooter.focus();
+        aEvent.preventDefault();
+        return;
+      }
+    }
+
+    // Pass keypress events to the richlistbox view when it's focused.
+    if (document.activeElement === richListBox) {
+      DownloadsView.onDownloadKeyPress(aEvent);
+    }
   },
 
   /**
-   * Keydown listener that listens for the accel-V "paste" event. Initiates a
-   * file download if the pasted item can be resolved to a URI.
+   * Keydown listener that listens for the keys to start key focusing, as well
+   * as the the accel-V "paste" event, which initiates a file download if the
+   * pasted item can be resolved to a URI.
    */
   _onKeyDown: function DP__onKeyDown(aEvent)
   {
+    // If the footer is focused and the downloads list has at least 1 element
+    // in it, focus the last element in the list when going up.
+    if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP &&
+        document.activeElement.parentNode.id === "downloadsFooter" &&
+        DownloadsView.richListBox.firstChild) {
+      DownloadsView.richListBox.focus();
+      DownloadsView.richListBox.selectedItem = DownloadsView.richListBox.lastChild;
+      aEvent.preventDefault();
+      return;
+    }
+
     let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V &&
 #ifdef XP_MACOSX
                   aEvent.metaKey;
 #else
                   aEvent.ctrlKey;
 #endif
 
     if (!pasting) {
@@ -359,17 +483,17 @@ const DownloadsPanel = {
     let element = document.commandDispatcher.focusedElement;
     while (element && element != this.panel) {
       element = element.parentNode;
     }
     if (!element) {
       if (DownloadsView.richListBox.itemCount > 0) {
         DownloadsView.richListBox.focus();
       } else {
-        this.panel.focus();
+        DownloadsFooter.focus();
       }
     }
   },
 
   /**
    * Opens the downloads panel when data is ready to be displayed.
    */
   _openPopupIfDataReady: function DP_openPopupIfDataReady()
@@ -762,46 +886,36 @@ const DownloadsView = {
   {
     // Handle primary clicks only, and exclude the action button.
     if (aEvent.button == 0 &&
         !aEvent.originalTarget.hasAttribute("oncommand")) {
       goDoCommand("downloadsCmd_open");
     }
   },
 
+  /**
+   * Handles keypress events on a download item.
+   */
   onDownloadKeyPress: function DV_onDownloadKeyPress(aEvent)
   {
-    // Handle unmodified keys only.
-    if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
-      return;
-    }
-
     // Pressing the key on buttons should not invoke the action because the
     // event has already been handled by the button itself.
     if (aEvent.originalTarget.hasAttribute("command") ||
         aEvent.originalTarget.hasAttribute("oncommand")) {
       return;
     }
 
     if (aEvent.charCode == " ".charCodeAt(0)) {
       goDoCommand("downloadsCmd_pauseResume");
       return;
     }
 
-    switch (aEvent.keyCode) {
-      case KeyEvent.DOM_VK_ENTER:
-      case KeyEvent.DOM_VK_RETURN:
-        goDoCommand("downloadsCmd_doDefault");
-        break;
-      case KeyEvent.DOM_VK_DOWN:
-        // Are we focused on the last element in the list?
-        if (this.richListBox.currentIndex == (this.richListBox.itemCount - 1)) {
-          DownloadsFooter.focus();
-        }
-        break;
+    if (aEvent.keyCode == KeyEvent.DOM_VK_ENTER ||
+        aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
+      goDoCommand("downloadsCmd_doDefault");
     }
   },
 
   onDownloadContextMenu: function DV_onDownloadContextMenu(aEvent)
   {
     let element = this.richListBox.selectedItem;
     if (!element) {
       return;
@@ -1320,17 +1434,17 @@ DownloadsViewItemController.prototype = 
 
 /**
  * Manages the summary at the bottom of the downloads panel list if the number
  * of items in the list exceeds the panels limit.
  */
 const DownloadsSummary = {
 
   /**
-   * Sets the active state of the summary. When active, the sumamry subscribes
+   * Sets the active state of the summary. When active, the summary subscribes
    * to the DownloadsCommon DownloadsSummaryData singleton.
    *
    * @param aActive
    *        Set to true to activate the summary.
    */
   set active(aActive)
   {
     if (aActive == this._active || !this._summaryNode) {
@@ -1426,22 +1540,22 @@ const DownloadsSummary = {
   focus: function()
   {
     if (this._summaryNode) {
       this._summaryNode.focus();
     }
   },
 
   /**
-   * Respond to keypress events on the Downloads Summary node.
+   * Respond to keydown events on the Downloads Summary node.
    *
    * @param aEvent
-   *        The keypress event being handled.
+   *        The keydown event being handled.
    */
-  onKeyPress: function DS_onKeyPress(aEvent)
+  onKeyDown: function DS_onKeyDown(aEvent)
   {
     if (aEvent.charCode == " ".charCodeAt(0) ||
         aEvent.keyCode == KeyEvent.DOM_VK_ENTER ||
         aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
       DownloadsPanel.showDownloadsHistory();
     }
   },
 
@@ -1522,50 +1636,38 @@ const DownloadsFooter = {
 
   /**
    * Focuses the appropriate element within the footer. If the summary
    * is visible, focus it. If not, focus the "Show All Downloads"
    * button.
    */
   focus: function DF_focus()
   {
-    if (DownloadsSummary.visible) {
+    if (this._showingSummary) {
       DownloadsSummary.focus();
     } else {
       DownloadsView.downloadsHistory.focus();
     }
   },
 
-  /**
-   * Handles keypress events on the footer element.
-   */
-  onKeyPress: function DF_onKeyPress(aEvent)
-  {
-    // If the up key is pressed, and the downloads list has at least 1 element
-    // in it, focus the last element in the list.
-    if (aEvent.keyCode == KeyEvent.DOM_VK_UP &&
-        DownloadsView.richListBox.itemCount > 0) {
-      DownloadsView.richListBox.focus();
-      DownloadsView.richListBox.selectedIndex =
-        (DownloadsView.richListBox.itemCount - 1);
-    }
-  },
+  _showingSummary: false,
 
   /**
    * Sets whether or not the Downloads Summary should be displayed in the
    * footer. If not, the "Show All Downloads" button is shown instead.
    */
   set showingSummary(aValue)
   {
     if (this._footerNode) {
       if (aValue) {
         this._footerNode.setAttribute("showingsummary", "true");
       } else {
         this._footerNode.removeAttribute("showingsummary");
       }
+      this._showingSummary = aValue;
     }
     return aValue;
   },
 
   /**
    * Element corresponding to the footer of the downloads panel.
    */
   get _footerNode()
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -96,26 +96,24 @@
                   label="&cmd.clearList.label;"
                   accesskey="&cmd.clearList.accesskey;"/>
       </menupopup>
 
       <richlistbox id="downloadsListBox"
                    class="plain"
                    flex="1"
                    context="downloadsContextMenu"
-                   onkeypress="DownloadsView.onDownloadKeyPress(event);"
                    oncontextmenu="DownloadsView.onDownloadContextMenu(event);"
                    ondragstart="DownloadsView.onDownloadDragStart(event);"/>
 
-      <vbox id="downloadsFooter"
-            onkeypress="DownloadsFooter.onKeyPress(event);">
+      <vbox id="downloadsFooter">
         <hbox id="downloadsSummary"
               align="center"
               orient="horizontal"
-              onkeypress="DownloadsSummary.onKeyPress(event);"
+              onkeydown="DownloadsSummary.onKeyDown(event);"
               onclick="DownloadsSummary.onClick(event);">
           <image class="downloadTypeIcon" />
           <vbox>
             <description id="downloadsSummaryDescription"
                          class="downloadTarget"
                          style="min-width: &downloadsSummary.minWidth2;"/>
             <progressmeter id="downloadsSummaryProgress"
                            class="downloadProgress"
--- a/browser/themes/gnomestripe/downloads/downloads.css
+++ b/browser/themes/gnomestripe/downloads/downloads.css
@@ -28,17 +28,17 @@
   border-top: 1px solid ThreeDShadow;
   background-image: linear-gradient(hsla(0,0%,0%,.15), hsla(0,0%,0%,.08) 6px);
 }
 
 #downloadsHistory > .button-box {
   margin: 1em;
 }
 
-#downloadsHistory:-moz-focusring > .button-box {
+#downloadsPanel[keyfocus] > #downloadsFooter > #downloadsHistory:focus > .button-box {
   outline: 1px -moz-dialogtext dotted;
 }
 
 /*** Downloads Summary and List items ***/
 
 #downloadsSummary,
 richlistitem[type="download"] {
   height: 6em;
@@ -47,17 +47,17 @@ richlistitem[type="download"] {
 }
 
 #downloadsSummary {
   padding: 8px 38px 8px 12px;
   cursor: pointer;
   -moz-user-focus: normal;
 }
 
-#downloadsSummary:-moz-focusring {
+#downloadsPanel[keyfocus] > #downloadsFooter > #downloadsSummary:focus {
   outline: 1px -moz-dialogtext dotted;
   outline-offset: -5px;
 }
 
 #downloadsSummary > .downloadTypeIcon {
   list-style-image: url("chrome://browser/skin/downloads/download-summary.png");
 }
 
@@ -76,17 +76,17 @@ richlistitem[type="download"] {
 richlistitem[type="download"]:first-child {
   border-top: 1px solid transparent;
 }
 
 richlistitem[type="download"]:last-child {
   border-bottom: 1px solid transparent;
 }
 
-#downloadsListBox:-moz-focusring > richlistitem[type="download"][selected] {
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected] {
   outline: 1px -moz-dialogtext dotted;
   outline-offset: -1px;
 }
 
 .downloadTypeIcon {
   -moz-margin-end: 8px;
   /* Prevent flickering when changing states. */
   min-height: 32px;
@@ -137,22 +137,23 @@ richlistitem[type="download"]:last-child
   padding: 5px;
   list-style-image: url("chrome://browser/skin/downloads/buttons.png");
 }
 
 .downloadButton > .button-box {
   padding: 0;
 }
 
-.downloadButton:-moz-focusring > .button-box {
+.downloadButton:focus > .button-box {
   outline: 1px -moz-dialogtext dotted;
 }
+
 /*** Highlighted list items ***/
 
-richlistitem[type="download"][state="1"]:hover {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover {
   border-radius: 3px;
   border-top: 1px solid hsla(0,0%,100%,.3);
   border-bottom: 1px solid hsla(0,0%,0%,.2);
   background-color: Highlight;
   background-image: linear-gradient(hsla(0,0%,100%,.1), hsla(0,0%,100%,0));
   color: HighlightText;
   cursor: pointer;
 }
@@ -170,28 +171,26 @@ richlistitem[type="download"]:hover > st
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadCancel:active {
   -moz-image-region: rect(0px, 64px, 16px, 48px);
 }
 
 .downloadButton.downloadShow {
   -moz-image-region: rect(16px, 16px, 32px, 0px);
 }
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow {
   -moz-image-region: rect(16px, 96px, 32px, 80px);
 }
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover {
   -moz-image-region: rect(16px, 112px, 32px, 96px);
 }
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active {
   -moz-image-region: rect(16px, 128px, 32px, 112px);
 }
 
-/** TODO: state="1"**/
-
 .downloadButton.downloadRetry {
   -moz-image-region: rect(32px, 16px, 48px, 0px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry {
   -moz-image-region: rect(32px, 32px, 48px, 16px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry:hover {
   -moz-image-region: rect(32px, 48px, 48px, 32px);
--- a/browser/themes/pinstripe/downloads/downloads.css
+++ b/browser/themes/pinstripe/downloads/downloads.css
@@ -40,23 +40,30 @@
   box-shadow: 0 -1px hsla(0,0%,100%,.5) inset, 0 1px 1px hsla(0,0%,0%,.03) inset;
 }
 
 #downloadsHistory > .button-box {
   color: #808080;
   margin: 1em;
 }
 
-#downloadsHistory:-moz-focusring > .button-box {
-  outline: 1px -moz-dialogtext dotted;
-  border-top-left-radius: 6px;
-  border-top-right-radius: 6px;
+#downloadsPanel[keyfocus] > #downloadsFooter > #downloadsSummary:focus,
+#downloadsPanel[keyfocus] > #downloadsFooter > #downloadsHistory:focus {
+  outline: 2px -moz-mac-focusring solid;
+  outline-offset: -2px;
+  -moz-outline-radius-bottomleft: 5px;
+  -moz-outline-radius-bottomright: 5px;
 }
 
-#downloadsPanel:not([hasdownloads]) > #downloadsFooter > #downloadsHistory:-moz-focusring > .button-box {
+#downloadsPanel:not([hasdownloads]) > #downloadsFooter > #downloadsHistory:focus {
+  -moz-outline-radius-topleft: 5px;
+  -moz-outline-radius-topright: 5px;
+}
+
+#downloadsPanel:not([hasdownloads]) > #downloadsFooter > #downloadsHistory:focus > .button-box {
   border-bottom-left-radius: 6px;
   border-bottom-right-radius: 6px;
 }
 
 /*** Downloads Summary and List items ***/
 
 #downloadsSummary,
 richlistitem[type="download"] {
@@ -66,21 +73,16 @@ richlistitem[type="download"] {
 }
 
 #downloadsSummary {
   padding: 8px 38px 8px 12px;
   cursor: pointer;
   -moz-user-focus: normal;
 }
 
-#downloadsSummary:-moz-focusring {
-  outline: 1px -moz-dialogtext dotted;
-  outline-offset: -5px;
-}
-
 #downloadsSummary > .downloadTypeIcon {
   list-style-image: url("chrome://browser/skin/downloads/download-summary.png");
 }
 
 @media (min-resolution: 2dppx) {
   #downloadsSummary > .downloadTypeIcon {
     list-style-image: url("chrome://browser/skin/downloads/download-summary@2x.png");
   }
@@ -101,21 +103,16 @@ richlistitem[type="download"] {
 richlistitem[type="download"]:first-child {
   border-top: 1px solid transparent;
 }
 
 richlistitem[type="download"]:last-child {
   border-bottom: 1px solid transparent;
 }
 
-#downloadsListBox:-moz-focusring > richlistitem[type="download"][selected] {
-  outline: 1px -moz-dialogtext dotted;
-  outline-offset: -1px;
-}
-
 .downloadTypeIcon {
   -moz-margin-end: 8px;
   /* Prevent flickering when changing states. */
   height: 32px;
   width: 32px;
 }
 
 .blockedIcon {
@@ -157,33 +154,37 @@ richlistitem[type="download"]:last-child
   min-height: 0;
   margin: 3px;
   border: none;
   background: transparent;
   padding: 5px;
   list-style-image: url("chrome://browser/skin/downloads/buttons.png");
 }
 
+.downloadButton:focus > .button-box {
+  outline: 2px -moz-mac-focusring solid;
+  outline-offset: -2px;
+}
+
 .downloadButton > .button-box {
   padding: 0;
 }
 
-.downloadButton:-moz-focusring > .button-box {
-  outline: 1px -moz-dialogtext dotted;
-}
-
 /*** Highlighted list items ***/
 
-richlistitem[type="download"][state="1"]:hover {
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected],
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover {
   border-radius: 3px;
   border-top: 1px solid hsla(0,0%,100%,.2);
   border-bottom: 1px solid hsla(0,0%,0%,.4);
   background-color: Highlight;
-  background-image: linear-gradient(hsl(210,100%,50%), hsl(210,96%,41%));
   color: HighlightText;
+}
+
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover {
   cursor: pointer;
 }
 
 /*** Button icons ***/
 
 .downloadButton.downloadCancel {
   -moz-image-region: rect(0px, 16px, 16px, 0px);
 }
@@ -191,42 +192,81 @@ richlistitem[type="download"]:hover > st
   -moz-image-region: rect(0px, 32px, 16px, 16px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadCancel:hover {
   -moz-image-region: rect(0px, 48px, 16px, 32px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadCancel:active {
   -moz-image-region: rect(0px, 64px, 16px, 48px);
 }
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected] > stack > .downloadButton.downloadCancel {
+  -moz-image-region: rect(0px, 80px, 16px, 64px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadCancel {
+  -moz-image-region: rect(0px, 96px, 16px, 80px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadCancel:hover {
+  -moz-image-region: rect(0px, 112px, 16px, 96px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadCancel:active {
+  -moz-image-region: rect(0px, 128px, 16px, 112px);
+}
 
 .downloadButton.downloadShow {
   -moz-image-region: rect(16px, 16px, 32px, 0px);
 }
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow {
+#downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover:not[selected] > stack > .downloadButton.downloadShow {
+  -moz-image-region: rect(16px, 32px, 32px, 16px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover:not[selected] > stack > .downloadButton.downloadShow:hover {
+  -moz-image-region: rect(16px, 48px, 32px, 32px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover:not[selected] > stack > .downloadButton.downloadShow:active {
+  -moz-image-region: rect(16px, 64px, 32px, 48px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected] > stack > .downloadButton.downloadShow {
+  -moz-image-region: rect(16px, 80px, 32px, 64px);
+}
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow,
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadShow {
   -moz-image-region: rect(16px, 96px, 32px, 80px);
 }
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover,
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadShow:hover {
   -moz-image-region: rect(16px, 112px, 32px, 96px);
 }
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active,
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadShow:active {
   -moz-image-region: rect(16px, 128px, 32px, 112px);
 }
 
 .downloadButton.downloadRetry {
   -moz-image-region: rect(32px, 16px, 48px, 0px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry {
   -moz-image-region: rect(32px, 32px, 48px, 16px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry:hover {
   -moz-image-region: rect(32px, 48px, 48px, 32px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry:active {
   -moz-image-region: rect(32px, 64px, 48px, 48px);
 }
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected] > stack > .downloadButton.downloadRetry {
+  -moz-image-region: rect(32px, 80px, 48px, 64px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadRetry {
+  -moz-image-region: rect(32px, 96px, 48px, 80px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadRetry:hover {
+  -moz-image-region: rect(32px, 112px, 48px, 96px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadRetry:active {
+  -moz-image-region: rect(32px, 128px, 48px, 112px);
+}
 
 @media (min-resolution: 2dppx) {
   .downloadButton {
     list-style-image: url("chrome://browser/skin/downloads/buttons@2x.png");
   }
   .downloadButton > .button-box > .button-icon {
     width: 16px;
     height: 16px;
@@ -239,42 +279,81 @@ richlistitem[type="download"]:hover > st
     -moz-image-region: rect(0px, 64px, 32px, 32px);
   }
   richlistitem[type="download"]:hover > stack > .downloadButton.downloadCancel:hover {
     -moz-image-region: rect(0px, 96px, 32px, 64px);
   }
   richlistitem[type="download"]:hover > stack > .downloadButton.downloadCancel:active {
     -moz-image-region: rect(0px, 128px, 32px, 96px);
   }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected] > stack > .downloadButton.downloadCancel {
+    -moz-image-region: rect(0px, 160px, 32px, 128px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadCancel {
+    -moz-image-region: rect(0px, 192px, 32px, 160px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadCancel:hover {
+    -moz-image-region: rect(0px, 224px, 32px, 192px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadCancel:active {
+    -moz-image-region: rect(0px, 256px, 32px, 224px);
+  }
 
   .downloadButton.downloadShow {
     -moz-image-region: rect(32px, 32px, 64px, 0px);
   }
-  richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow {
+  #downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover:not[selected] > stack > .downloadButton.downloadShow {
+    -moz-image-region: rect(32px, 64px, 64px, 32px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover:not[selected] > stack > .downloadButton.downloadShow:hover {
+    -moz-image-region: rect(32px, 96px, 64px, 64px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover:not[selected] > stack > .downloadButton.downloadShow:active {
+    -moz-image-region: rect(32px, 128px, 64px, 96px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected] > stack > .downloadButton.downloadShow {
+    -moz-image-region: rect(32px, 160px, 64px, 128px);
+  }
+  #downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow,
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadShow {
     -moz-image-region: rect(32px, 192px, 64px, 160px);
   }
-  richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover {
+  #downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover,
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadShow:hover {
     -moz-image-region: rect(32px, 224px, 64px, 192px);
   }
-  richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active {
+  #downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active,
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadShow:active {
     -moz-image-region: rect(32px, 256px, 64px, 224px);
   }
 
   .downloadButton.downloadRetry {
     -moz-image-region: rect(64px, 32px, 96px, 0px);
   }
   richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry {
     -moz-image-region: rect(64px, 64px, 96px, 32px);
   }
   richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry:hover {
     -moz-image-region: rect(64px, 96px, 96px, 64px);
   }
   richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry:active {
     -moz-image-region: rect(64px, 128px, 96px, 96px);
   }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected] > stack > .downloadButton.downloadRetry {
+    -moz-image-region: rect(64px, 160px, 96px, 128px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadRetry {
+    -moz-image-region: rect(64px, 192px, 96px, 160px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadRetry:hover {
+    -moz-image-region: rect(64px, 224px, 96px, 192px);
+  }
+  #downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"]:hover[selected] > stack > .downloadButton.downloadRetry:active {
+    -moz-image-region: rect(64px, 256px, 96px, 224px);
+  }
 }
 
 /*** Status and progress indicator ***/
 
 #downloads-indicator-anchor {
   min-width: 20px;
   min-height: 20px;
   /* Makes the outermost stack element positioned, so that its contents are
--- a/browser/themes/winstripe/downloads/downloads-aero.css
+++ b/browser/themes/winstripe/downloads/downloads-aero.css
@@ -7,17 +7,17 @@
 %undef WINSTRIPE_AERO
 
 @media (-moz-windows-default-theme) {
   richlistitem[type="download"] {
     border: 1px solid transparent;
     border-bottom: 1px solid hsl(213,40%,90%);
   }
 
-  richlistitem[type="download"][state="1"]:hover {
+  #downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover {
     border: 1px solid hsl(213,45%,65%);
     box-shadow: 0 0 0 1px hsla(0,0%,100%,.5) inset,
                 0 1px 0 hsla(0,0%,100%,.3) inset;
     background-image: linear-gradient(hsl(212,86%,92%), hsl(212,91%,86%));
     color: black;
   }
 }
 
--- a/browser/themes/winstripe/downloads/downloads.css
+++ b/browser/themes/winstripe/downloads/downloads.css
@@ -19,17 +19,23 @@
 }
 
 #downloadsHistory {
   background: transparent;
   color: -moz-nativehyperlinktext;
   cursor: pointer;
 }
 
+#downloadsPanel[keyfocus] > #downloadsFooter > #downloadsHistory:focus {
+  outline: 1px -moz-dialogtext dotted;
+  outline-offset: -1px;
+}
+
 #downloadsHistory > .button-box {
+  border: none;
   margin: 1em;
 }
 
 @media (-moz-windows-default-theme) {
   #downloadsPanel[hasdownloads] > #downloadsFooter {
 %ifdef WINSTRIPE_AERO
     background-color: #f1f5fb;
 %else
@@ -51,17 +57,17 @@ richlistitem[type="download"] {
 }
 
 #downloadsSummary {
   padding: 8px 38px 8px 12px;
   cursor: pointer;
   -moz-user-focus: normal;
 }
 
-#downloadsSummary:-moz-focusring {
+#downloadsPanel[keyfocus] > #downloadsFooter > #downloadsSummary:focus {
   outline: 1px -moz-dialogtext dotted;
   outline-offset: -5px;
 }
 
 #downloadsSummary > .downloadTypeIcon {
   list-style-image: url("chrome://browser/skin/downloads/download-summary.png");
 }
 
@@ -82,17 +88,17 @@ richlistitem[type="download"]:first-chil
 }
 
 @media (-moz-windows-default-theme) {
   richlistitem[type="download"]:last-child {
     border-bottom: 1px solid transparent;
   }
 }
 
-#downloadsListBox:-moz-focusring > richlistitem[type="download"][selected] {
+#downloadsPanel[keyfocus] > #downloadsListBox:focus > richlistitem[type="download"][selected] {
   outline: 1px -moz-dialogtext dotted;
   outline-offset: -1px;
 }
 
 .downloadTypeIcon {
   -moz-margin-end: 8px;
   /* Prevent flickering when changing states. */
   min-height: 32px;
@@ -139,22 +145,27 @@ richlistitem[type="download"]:first-chil
   margin: 3px;
   border: none;
   background: transparent;
   padding: 5px;
   list-style-image: url("chrome://browser/skin/downloads/buttons.png");
 }
 
 .downloadButton > .button-box {
+  border: 1px solid transparent;
   padding: 0;
 }
 
+#downloadsPanel[keyfocus] .downloadButton:focus > .button-box {
+  border: 1px dotted ThreeDDarkShadow;
+}
+
 /*** Highlighted list items ***/
 
-richlistitem[type="download"][state="1"]:hover {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover {
   border-radius: 3px;
   border-top: 1px solid hsla(0,0%,100%,.2);
   border-bottom: 1px solid hsla(0,0%,0%,.2);
   background-color: Highlight;
   color: HighlightText;
   cursor: pointer;
 }
 
@@ -181,23 +192,32 @@ richlistitem[type="download"]:hover > st
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadShow:hover {
   -moz-image-region: rect(16px, 48px, 32px, 32px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadShow:active {
   -moz-image-region: rect(16px, 64px, 32px, 48px);
 }
 %ifndef WINSTRIPE_AERO
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow {
+#downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow {
+  -moz-image-region: rect(16px, 32px, 32px, 16px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover {
+  -moz-image-region: rect(16px, 48px, 32px, 32px);
+}
+#downloadsPanel[keyfocus] > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active {
+  -moz-image-region: rect(16px, 64px, 32px, 48px);
+}
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow {
   -moz-image-region: rect(16px, 96px, 32px, 80px);
 }
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:hover {
   -moz-image-region: rect(16px, 112px, 32px, 96px);
 }
-richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active {
+#downloadsPanel:not([keyfocus]) > #downloadsListBox > richlistitem[type="download"][state="1"]:hover > stack > .downloadButton.downloadShow:active {
   -moz-image-region: rect(16px, 128px, 32px, 112px);
 }
 %endif
 
 .downloadButton.downloadRetry {
   -moz-image-region: rect(32px, 16px, 48px, 0px);
 }
 richlistitem[type="download"]:hover > stack > .downloadButton.downloadRetry {