Bug 1602372 - Move focus ring through compose input fields on F6 keypress. r=mkmelin
authorAlessandro Castellani <alessandro@thunderbird.net>
Mon, 27 Jan 2020 10:44:47 -0800
changeset 38041 91f77a3708ffa978462e80a87764ca364ad669f6
parent 38040 fefcbe1ff081081f1c99e7a0549889a3e24a7e0d
child 38042 4a36f8aa2dddb1de9e2288ef6f53697c82fbf1f2
push id398
push userclokep@gmail.com
push dateMon, 09 Mar 2020 19:10:28 +0000
reviewersmkmelin
bugs1602372
Bug 1602372 - Move focus ring through compose input fields on F6 keypress. r=mkmelin
mail/components/compose/content/MsgComposeCommands.js
mail/test/browser/composition/browser_focus.js
--- a/mail/components/compose/content/MsgComposeCommands.js
+++ b/mail/components/compose/content/MsgComposeCommands.js
@@ -6967,17 +6967,22 @@ function DisplaySaveFolderDlg(folderURI)
     );
     try {
       gCurrentIdentity.showSaveMsgDlg = !checkbox.value;
     } catch (e) {}
   }
 }
 
 function SetMsgToRecipientElementFocus() {
-  document.getElementById("toAddrInput").focus();
+  if (!document.getElementById("addressRowTo").classList.contains("hidden")) {
+    document.getElementById("toAddrInput").focus();
+    return;
+  }
+
+  SetFocusOnNextAvailableElement(document.getElementById("toAddrInput"));
 }
 
 function SetMsgIdentityElementFocus() {
   document.getElementById("msgIdentity").focus();
 }
 
 function SetMsgSubjectElementFocus() {
   document.getElementById("msgSubject").focus();
@@ -7070,17 +7075,25 @@ function WhichElementHasFocus() {
     return window.content;
   }
 
   let currentNode = top.document.commandDispatcher.focusedElement;
   while (currentNode) {
     if (
       currentNode == document.getElementById("msgIdentity") ||
       currentNode == document.getElementById("toAddrInput") ||
+      currentNode == document.getElementById("ccAddrInput") ||
+      currentNode == document.getElementById("bccAddrInput") ||
+      currentNode == document.getElementById("replyAddrInput") ||
+      currentNode == document.getElementById("newsgroupsAddrInput") ||
+      currentNode == document.getElementById("followupAddrInput") ||
       currentNode == document.getElementById("msgSubject") ||
+      currentNode == document.getElementById("extraRecipientsLabel") ||
+      currentNode == document.getElementById("addr_bcc") ||
+      currentNode == document.getElementById("addr_cc") ||
       currentNode == msgAttachmentElement ||
       currentNode == abContactsPanelElement
     ) {
       return currentNode;
     }
     currentNode = currentNode.parentNode;
   }
 
@@ -7107,16 +7120,31 @@ function SwitchElementFocus(event) {
     // of the focus ring.
     SetMsgToRecipientElementFocus();
     return;
   }
 
   if (event && event.shiftKey) {
     // Backwards focus ring: e.g. Ctrl+Shift+Tab | Shift+F6
     switch (focusedElement) {
+      case document.getElementById("newsgroupsAddrInput"):
+        SetFocusOnPreviousAvailableElement(focusedElement);
+        break;
+      case document.getElementById("followupAddrInput"):
+        SetFocusOnPreviousAvailableElement(focusedElement);
+        break;
+      case document.getElementById("replyAddrInput"):
+        SetFocusOnPreviousAvailableElement(focusedElement);
+        break;
+      case document.getElementById("bccAddrInput"):
+        SetFocusOnPreviousAvailableElement(focusedElement);
+        break;
+      case document.getElementById("ccAddrInput"):
+        SetFocusOnPreviousAvailableElement(focusedElement);
+        break;
       case document.getElementById("toAddrInput"):
         SetMsgIdentityElementFocus();
         break;
       case document.getElementById("msgIdentity"):
         // Focus the search input of contacts side bar if that's available,
         // otherwise focus message body.
         if (sidebar_is_hidden() || !focusContactsSidebarSearchInput()) {
           SetMsgBodyFrameFocus();
@@ -7131,54 +7159,135 @@ function SwitchElementFocus(event) {
           SetMsgAttachmentElementFocus();
         } else {
           SetMsgSubjectElementFocus();
         }
         break;
       case gMsgAttachmentElement:
         SetMsgSubjectElementFocus();
         break;
+      case document.getElementById("msgSubject"):
+        SetFocusOnPreviousAvailableElement(focusedElement);
+        break;
       default:
-        // document.getElementById("msgSubject")
+        // focus on '#msgIdentity'
         SetMsgToRecipientElementFocus();
         break;
     }
-  } else {
-    // Forwards focus ring: e.g. Ctrl+Tab | F6
-    switch (focusedElement) {
-      case document.getElementById("toAddrInput"):
-        SetMsgSubjectElementFocus();
-        break;
-      case document.getElementById("msgSubject"):
-        // Only set focus to the attachment element if it is shown.
-        if (!document.getElementById("attachments-box").collapsed) {
-          SetMsgAttachmentElementFocus();
-        } else {
-          SetMsgBodyFrameFocus();
-        }
-        break;
-      case gMsgAttachmentElement:
+
+    return;
+  }
+
+  // Forwards focus ring: e.g. Ctrl+Tab | F6
+  switch (focusedElement) {
+    case document.getElementById("toAddrInput"):
+      SetFocusOnNextAvailableElement(focusedElement);
+      break;
+    case document.getElementById("ccAddrInput"):
+      SetFocusOnNextAvailableElement(focusedElement);
+      break;
+    case document.getElementById("bccAddrInput"):
+      SetFocusOnNextAvailableElement(focusedElement);
+      break;
+    case document.getElementById("replyAddrInput"):
+      SetFocusOnNextAvailableElement(focusedElement);
+      break;
+    case document.getElementById("followupAddrInput"):
+      SetFocusOnNextAvailableElement(focusedElement);
+      break;
+    case document.getElementById("newsgroupsAddrInput"):
+      SetFocusOnNextAvailableElement(focusedElement);
+      break;
+    case document.getElementById("msgSubject"):
+      // Only set focus to the attachment element if it is shown.
+      if (!document.getElementById("attachments-box").collapsed) {
+        SetMsgAttachmentElementFocus();
+      } else {
         SetMsgBodyFrameFocus();
-        break;
-      case window.content:
-        // Focus the search input of contacts side bar if that's available,
-        // otherwise focus "From" selector.
-        if (sidebar_is_hidden() || !focusContactsSidebarSearchInput()) {
-          SetMsgIdentityElementFocus();
-        }
-        break;
-      case sidebarDocumentGetElementById("abContactsPanel"):
+      }
+      break;
+    case gMsgAttachmentElement:
+      SetMsgBodyFrameFocus();
+      break;
+    case window.content:
+      // Focus the search input of contacts side bar if that's available,
+      // otherwise focus "From" selector.
+      if (sidebar_is_hidden() || !focusContactsSidebarSearchInput()) {
         SetMsgIdentityElementFocus();
-        break;
-      default:
-        // document.getElementById("msgIdentity")
-        SetMsgToRecipientElementFocus();
-        break;
-    }
-  }
+      }
+      break;
+    case sidebarDocumentGetElementById("abContactsPanel"):
+      SetMsgIdentityElementFocus();
+      break;
+    default:
+      SetMsgToRecipientElementFocus();
+      break;
+  }
+}
+
+/**
+ * Find the closest visible previous element in the list of recipients
+ * and move the focus on its autocomplete input field.
+ *
+ * @param {HTMLElement} element - The currently focused element.
+ */
+function SetFocusOnPreviousAvailableElement(element) {
+  // If the current element is msgSubject we need to select the last not hidden
+  // row in the mail-recipients-area.
+  if (element == document.getElementById("msgSubject")) {
+    element = document.getElementById("recipientsContainer").lastChild;
+
+    // If the last available address-row child is not hidden, grab the focus.
+    if (!element.classList.contains("hidden")) {
+      element
+        .querySelector(`input[is="autocomplete-input"][recipienttype]`)
+        .focus();
+      return;
+    }
+  }
+
+  // If a previous address row is abailable and not hidden,
+  // focus on the autocomplete input field.
+  let previousRow = element.closest(".address-row").previousElementSibling;
+  while (previousRow) {
+    if (!previousRow.classList.contains("hidden")) {
+      previousRow
+        .querySelector(`input[is="autocomplete-input"][recipienttype]`)
+        .focus();
+      return;
+    }
+    previousRow = previousRow.previousElementSibling;
+  }
+
+  // Move the focus on the msgIdentity if no extra recipients are available.
+  SetMsgIdentityElementFocus();
+}
+
+/**
+ * Find the closest visible next element in the list of recipients
+ * and move the focus on its autocomplete input field.
+ *
+ * @param {HTMLElement} element - The currently focused element.
+ */
+function SetFocusOnNextAvailableElement(element) {
+  // If a next address row is abailable and not hidden,
+  // focus on the autocomplete input field.
+  let nextRow = element.closest(".address-row").nextElementSibling;
+  while (nextRow) {
+    if (!nextRow.classList.contains("hidden")) {
+      nextRow
+        .querySelector(`input[is="autocomplete-input"][recipienttype]`)
+        .focus();
+      return;
+    }
+    nextRow = nextRow.nextElementSibling;
+  }
+
+  // Move the focus on the msgSubject if no extra recipients are available.
+  SetMsgSubjectElementFocus();
 }
 
 function sidebarCloseButtonOnCommand() {
   toggleAddressPicker();
 }
 
 /**
  * Show or hide contacts side bar,
--- a/mail/test/browser/composition/browser_focus.js
+++ b/mail/test/browser/composition/browser_focus.js
@@ -22,21 +22,29 @@ var { mc } = ChromeUtils.import(
  * elements forward and backward.
  *
  * @param controller the compose window controller
  * @param attachmentsExpanded true if the attachment pane is expanded
  * @param ctrlTab true if we should use Ctrl+Tab to cycle, false if we should
  *                use F6
  */
 function check_element_cycling(controller, attachmentsExpanded, ctrlTab) {
+  // Make sure the accessibility tabfocus is set to 7 to enable normal Tab
+  // focus on non-input field elements. This is necessary only for macOS as
+  // the dafault value is 2 instead of the default 7 used on Windows and Linux.
+  Services.prefs.setIntPref("accessibility.tabfocus", 7);
+
   let addressingElement = controller.e("toAddrInput");
   let subjectElement = controller.e("msgSubject");
   let attachmentElement = controller.e("attachmentBucket");
   let contentElement = controller.window.content;
   let identityElement = controller.e("msgIdentity");
+  let extraRecipientsLabel = controller.e("extraRecipientsLabel");
+  let bccLabel = controller.e("addr_bcc");
+  let ccLabel = controller.e("addr_cc");
 
   let key = ctrlTab ? "VK_TAB" : "VK_F6";
 
   // We start on the addressing widget and go from there.
 
   controller.keypress(null, key, { ctrlKey: ctrlTab });
   Assert.equal(subjectElement, controller.window.WhichElementHasFocus());
   if (attachmentsExpanded) {
@@ -47,28 +55,45 @@ function check_element_cycling(controlle
   Assert.equal(contentElement, controller.window.WhichElementHasFocus());
   controller.keypress(null, key, { ctrlKey: ctrlTab });
   Assert.equal(identityElement, controller.window.WhichElementHasFocus());
   controller.keypress(null, key, { ctrlKey: ctrlTab });
   mc.sleep(0); // Focusing the addressing element happens in a timeout...
   Assert.equal(addressingElement, controller.window.WhichElementHasFocus());
 
   controller.keypress(null, key, { ctrlKey: ctrlTab, shiftKey: true });
+
+  if (ctrlTab) {
+    Assert.equal(
+      extraRecipientsLabel,
+      controller.window.WhichElementHasFocus()
+    );
+    controller.keypress(null, key, { shiftKey: true });
+    Assert.equal(bccLabel, controller.window.WhichElementHasFocus());
+    controller.keypress(null, key, { shiftKey: true });
+    Assert.equal(ccLabel, controller.window.WhichElementHasFocus());
+
+    controller.keypress(null, key, { shiftKey: true });
+  }
+
   Assert.equal(identityElement, controller.window.WhichElementHasFocus());
   controller.keypress(null, key, { ctrlKey: ctrlTab, shiftKey: true });
   Assert.equal(contentElement, controller.window.WhichElementHasFocus());
   if (attachmentsExpanded) {
     controller.keypress(null, key, { ctrlKey: ctrlTab, shiftKey: true });
     Assert.equal(attachmentElement, controller.window.WhichElementHasFocus());
   }
   controller.keypress(null, key, { ctrlKey: ctrlTab, shiftKey: true });
   Assert.equal(subjectElement, controller.window.WhichElementHasFocus());
   controller.keypress(null, key, { ctrlKey: ctrlTab, shiftKey: true });
   mc.sleep(0); // Focusing the addressing element happens in a timeout...
   Assert.equal(addressingElement, controller.window.WhichElementHasFocus());
+
+  // Reset the preferences.
+  Services.prefs.clearUserPref("accessibility.tabfocus");
 }
 
 add_task(function test_f6_no_attachment() {
   let cwc = open_compose_new_mail();
   check_element_cycling(cwc, false, false);
   close_compose_window(cwc);
 });