Bug 819428 - Re-jig keyboard navigation for the Downloads Panel.
authorMike Conley <mconley@mozilla.com>
Mon, 14 Jan 2013 12:59:40 -0500
changeset 123604 55f3f74f19d3a67665b828feead5e3211f953e67
parent 123603 7da0f49120794305577b133d0b8c18e083cb2568
child 123605 4d1bd79fba777f8b066dfe16f7ad594381c5c52f
push id3178
push usermak77@bonardo.net
push dateTue, 15 Jan 2013 16:26:10 +0000
treeherdermozilla-aurora@8233d14cdc57 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs819428
milestone20.0a2
Bug 819428 - Re-jig keyboard navigation for the Downloads Panel. r=mak a=gavin
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 {