IPH accessibility: help message for help bubble accelerator

Help message is now read to the screen reader when an IPH bubble is
shown. It instructs the user that they can toggle focus between the IPH
bubble and (optionally) the bubble's anchor/target button using the
accelerator ALT+SHIFT+H (CMD+OPTION+SHIFT+H).

Still to do: limit the number of times this message is read, especially
after the user uses the accelerator.

Bug: 1212149
Change-Id: I914d365b241e0146d177ca58b5519dd585d5bb24
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3207758
Commit-Queue: Dana Fried <dfried@chromium.org>
Reviewed-by: Peter Boström <pbos@chromium.org>
Reviewed-by: Emily Shack <emshack@chromium.org>
Cr-Commit-Position: refs/heads/main@{#932792}
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 9d42f10..76f7678d 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -521,6 +521,12 @@
             This dialog is not currently focused. Press Alt-Shift A to focus this dialog.
           </message>
         </if>
+        <message name="IDS_FOCUS_HELP_BUBBLE_DESCRIPTION" is_accessibility_with_no_ui="true" desc="Message announced to screen reader users when a general help bubble is shown.">
+          Press |<ph name="ACCELERATOR">$1<ex>F6</ex></ph>| to focus this help bubble.
+        </message>
+        <message name="IDS_FOCUS_HELP_BUBBLE_TOGGLE_DESCRIPTION" is_accessibility_with_no_ui="true" desc="Message announced to screen reader when a help bubble pointing to a specific element is shown.">
+          Press |<ph name="ACCELERATOR">$1<ex>F6</ex></ph>| to toggle focus between this help bubble and the element it is pointing to.
+        </message>
       </if>
 
        <!-- content area context menus. Android does not use it -->
diff --git a/chrome/browser/ui/views/user_education/feature_promo_bubble_owner_impl.cc b/chrome/browser/ui/views/user_education/feature_promo_bubble_owner_impl.cc
index 7d9869d..69b9d3d 100644
--- a/chrome/browser/ui/views/user_education/feature_promo_bubble_owner_impl.cc
+++ b/chrome/browser/ui/views/user_education/feature_promo_bubble_owner_impl.cc
@@ -21,14 +21,13 @@
 
 bool FeaturePromoBubbleOwnerImpl::ToggleFocusForAccessibility() {
   // If the bubble isn't present or can't be meaningfully focused, stop.
-  if (!bubble_ || !bubble_->GetInitiallyFocusedView())
+  if (!bubble_)
     return false;
 
-  auto* const anchor = bubble_->GetAnchorView();
-
   // Focus can't be determined just by widget activity; we must check to see if
   // there's a focused view in the anchor widget or the top-level browser
   // widget (if different).
+  auto* const anchor = bubble_->GetAnchorView();
   const bool is_focus_in_ancestor_widget =
       (anchor && anchor->GetFocusManager()->GetFocusedView()) ||
       bubble_->GetWidget()
diff --git a/chrome/browser/ui/views/user_education/feature_promo_bubble_view.cc b/chrome/browser/ui/views/user_education/feature_promo_bubble_view.cc
index fb77870..7ffa273 100644
--- a/chrome/browser/ui/views/user_education/feature_promo_bubble_view.cc
+++ b/chrome/browser/ui/views/user_education/feature_promo_bubble_view.cc
@@ -260,6 +260,10 @@
                          ? params.body_text
                          : params.screenreader_text;
 
+  // If there's a keyboard navigation hint, append it after a full stop.
+  if (!params.keyboard_navigation_hint.empty())
+    accessible_name_ += u". " + params.keyboard_navigation_hint;
+
   // Since we don't have any controls for the user to interact with (we're just
   // an information bubble), override our role to kAlert.
   SetAccessibleRole(ax::mojom::Role::kAlert);
diff --git a/chrome/browser/ui/views/user_education/feature_promo_bubble_view.h b/chrome/browser/ui/views/user_education/feature_promo_bubble_view.h
index 1487c3a..afc2cf6 100644
--- a/chrome/browser/ui/views/user_education/feature_promo_bubble_view.h
+++ b/chrome/browser/ui/views/user_education/feature_promo_bubble_view.h
@@ -67,6 +67,10 @@
     std::u16string title_text;
     std::u16string screenreader_text;
 
+    // Additional message to be read to screen reader users to aid in
+    // navigation.
+    std::u16string keyboard_navigation_hint;
+
     std::vector<ButtonParams> buttons;
 
     absl::optional<int> preferred_width;
diff --git a/chrome/browser/ui/views/user_education/feature_promo_controller_views.cc b/chrome/browser/ui/views/user_education/feature_promo_controller_views.cc
index e718d73..83c7be5 100644
--- a/chrome/browser/ui/views/user_education/feature_promo_controller_views.cc
+++ b/chrome/browser/ui/views/user_education/feature_promo_controller_views.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/ui/views/user_education/feature_promo_controller_views.h"
 
+#include <string>
 #include <utility>
 
 #include "base/feature_list.h"
@@ -11,6 +12,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/token.h"
+#include "chrome/app/chrome_command_ids.h"
 #include "chrome/browser/feature_engagement/tracker_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/user_education/feature_promo_bubble_params.h"
@@ -25,6 +27,8 @@
 #include "components/feature_engagement/public/feature_constants.h"
 #include "components/feature_engagement/public/tracker.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
+#include "ui/accessibility/platform/ax_platform_node.h"
+#include "ui/base/accelerators/accelerator.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/views/bubble/bubble_border.h"
 #include "ui/views/style/platform_style.h"
@@ -426,6 +430,38 @@
         weak_ptr_factory_.GetWeakPtr(), *current_iph_feature_);
   }
 
+  if (CheckScreenReaderPromptAvailable()) {
+    ui::Accelerator accelerator;
+    std::u16string accelerator_text;
+    if (browser_view_->GetAccelerator(IDC_FOCUS_NEXT_PANE, &accelerator)) {
+      accelerator_text = accelerator.GetShortcutText();
+    } else {
+      NOTREACHED();
+    }
+
+    if (!params.focus_on_create && !params.timeout_no_interaction.has_value()) {
+      // No message is required as this is a background bubble with a
+      // screen reader-specific prompt and will dismiss itself.
+      LOG_IF(WARNING, !params.screenreader_string_specifier)
+          << "Toast feature promo without screenreader prompt: "
+          << create_params.body_text;
+    } else if (anchor_view->IsAccessibilityFocusable()) {
+      // Present the user with the full help bubble navigation shortcut.
+      create_params.keyboard_navigation_hint = l10n_util::GetStringFUTF16(
+          IDS_FOCUS_HELP_BUBBLE_TOGGLE_DESCRIPTION, accelerator_text);
+    } else if (!params.focus_on_create) {
+      // Present the user with an abridged help bubble navigation shortcut.
+      create_params.keyboard_navigation_hint = l10n_util::GetStringFUTF16(
+          IDS_FOCUS_HELP_BUBBLE_DESCRIPTION, accelerator_text);
+    } else {
+      // No prompt for a bubble that starts focused and where the anchor view
+      // cannot receive focus, since neither the help bubble accelerator nor F6
+      // would do anything. However, in this case we require a close button of
+      // some variety.
+      DCHECK(params.allow_snooze || params.show_close_button);
+    }
+  }
+
   bubble_id_ = bubble_owner_->ShowBubble(
       std::move(create_params),
       base::BindOnce(&FeaturePromoControllerViews::HandleBubbleClosed,
@@ -465,3 +501,8 @@
     current_critical_promo_.reset();
   }
 }
+
+bool FeaturePromoControllerViews::CheckScreenReaderPromptAvailable() const {
+  return ui::AXPlatformNode::GetAccessibilityMode().has_mode(
+      ui::AXMode::kScreenReader);
+}
diff --git a/chrome/browser/ui/views/user_education/feature_promo_controller_views.h b/chrome/browser/ui/views/user_education/feature_promo_controller_views.h
index 33315d2..0db9e2b 100644
--- a/chrome/browser/ui/views/user_education/feature_promo_controller_views.h
+++ b/chrome/browser/ui/views/user_education/feature_promo_controller_views.h
@@ -137,6 +137,10 @@
   void OnUserSnooze(const base::Feature& iph_feature);
   void OnUserDismiss(const base::Feature& iph_feature);
 
+  // Returns whether we can play a screen reader prompt for the "focus help
+  // bubble" promo.
+  bool CheckScreenReaderPromptAvailable() const;
+
   // The browser window this instance is responsible for.
   BrowserView* const browser_view_;
 
diff --git a/ui/base/accelerators/accelerator.cc b/ui/base/accelerators/accelerator.cc
index d6913b1..d207054 100644
--- a/ui/base/accelerators/accelerator.cc
+++ b/ui/base/accelerators/accelerator.cc
@@ -410,6 +410,9 @@
     case VKEY_F1:
       string_id = IDS_APP_F1_KEY;
       break;
+    case VKEY_F6:
+      string_id = IDS_APP_F6_KEY;
+      break;
     case VKEY_F11:
       string_id = IDS_APP_F11_KEY;
       break;
diff --git a/ui/strings/ui_strings.grd b/ui/strings/ui_strings.grd
index 29da65ce..0f57b794 100644
--- a/ui/strings/ui_strings.grd
+++ b/ui/strings/ui_strings.grd
@@ -703,6 +703,9 @@
       <message name="IDS_APP_F1_KEY" desc="F1 key">
         F1
       </message>
+      <message name="IDS_APP_F6_KEY" is_accessibility_with_no_ui="true" desc="F6 key">
+        F6
+      </message>
       <message name="IDS_APP_F11_KEY" desc="F11 key">
         F11
       </message>