diff --git a/chrome/browser/ui/views/javascript_tab_modal_dialog_view_views.cc b/chrome/browser/ui/views/javascript_tab_modal_dialog_view_views.cc index 684a1b2e2eef4d..d2c4198fd7cc62 100644 --- a/chrome/browser/ui/views/javascript_tab_modal_dialog_view_views.cc +++ b/chrome/browser/ui/views/javascript_tab_modal_dialog_view_views.cc @@ -49,6 +49,13 @@ void JavaScriptTabModalDialogViewViews::AddedToWidget() { bubble_frame_view->SetTitleView(CreateTitleOriginLabel(GetWindowTitle())); GetWidget()->GetRootView()->GetViewAccessibility().OverrideDescription( message_text_); + + // On some platforms, the platform accessibility API automatically + // calculates the name of the native window based on the child RootView. + // We override that calculation here so that we can present both the + // title (e.g. "url.com says") and the message text on platforms where + // the accessible description is ignored. + GetViewAccessibility().OverrideNativeWindowTitle(GetWindowTitle()); } JavaScriptTabModalDialogViewViews::JavaScriptTabModalDialogViewViews( diff --git a/chrome/browser/ui/views/javascript_tab_modal_dialog_view_views_browsertest_mac.mm b/chrome/browser/ui/views/javascript_tab_modal_dialog_view_views_browsertest_mac.mm new file mode 100644 index 00000000000000..e3a62027ec2282 --- /dev/null +++ b/chrome/browser/ui/views/javascript_tab_modal_dialog_view_views_browsertest_mac.mm @@ -0,0 +1,80 @@ +// Copyright 2022 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "base/bind.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/views/javascript_tab_modal_dialog_view_views.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "content/public/test/browser_test.h" +#include "ui/accessibility/platform/ax_platform_node_mac.h" +#include "ui/views/accessibility/view_accessibility.h" +#include "ui/views/bubble/bubble_frame_view.h" + +using JavaScriptTabModalDialogViewViewsBrowserTestMac = InProcessBrowserTest; + +IN_PROC_BROWSER_TEST_F(JavaScriptTabModalDialogViewViewsBrowserTestMac, + AlertDialogAccessibleNameDescriptionAndRole) { + std::u16string title = u"Title"; + std::u16string message = u"The message"; + auto* dialog_views = + JavaScriptTabModalDialogViewViews::CreateAlertDialogForTesting( + browser(), title, message); + + // For a JavaScript alert dialog, VoiceOver speaks the accessible name of + // the native window followed by the accessible name of the RootView. For + // reasons detailed below, we have to make some adjustments on the Mac to + // ensure that the window's name contains the title (e.g. "url.com says") + // and that the RootView's name contains the message text. This test verifies + // this exposure as well as exposure through other properties that VoiceOver + // ignores. + + // The RootView of a JavaScript alert dialog should have the accessible role + // of dialog. On the Mac, that is exposed as the subrole of a group. + gfx::NativeViewAccessible native_dialog = dialog_views->GetWidget() + ->GetRootView() + ->GetViewAccessibility() + .GetNativeObject(); + EXPECT_EQ(NSAccessibilityGroupRole, [native_dialog accessibilityRole]); + EXPECT_TRUE([@"AXApplicationDialog" + isEqualToString:(NSString*)[native_dialog accessibilitySubrole]]); + + // JavaScriptTabModalDialogViewViews sets the accessible description of the + // RootView to the message contents. That description is exposed on the Mac + // via accessibilityHelp. + EXPECT_EQ(message, + base::SysNSStringToUTF16([native_dialog accessibilityHelp])); + + // While some screen readers use the accessible description to know what to + // present to the user, VoiceOver currently does not. Therefore, we override + // the RootView's accessible name in ViewAXPlatformNodeDelegateMac. That name + // is then exposed as the accessibilityTitle (and not accessibilityLabel) on + // the Mac because in AXPlatformNodeCocoa, window roles (including dialog) do + // not expose an accessibilityLabel. + EXPECT_EQ(message, + base::SysNSStringToUTF16([native_dialog accessibilityTitle])); + EXPECT_EQ(u"", base::SysNSStringToUTF16([native_dialog accessibilityLabel])); + + // The parent of the native dialog should be a window. + gfx::NativeViewAccessible native_window = [native_dialog accessibilityParent]; + EXPECT_EQ(NSAccessibilityWindowRole, [native_window accessibilityRole]); + + // On the Mac, the native window's accessible title comes from the "contents" + // of the window. In this case, that is the accessibilityTitle of the RootView + // which we overrode as described above. As a result, the native window's + // accessibilityTitle is now also the message text. It is not necessary to + // unset it. (See next comment.) + EXPECT_EQ(message, + base::SysNSStringToUTF16([native_window accessibilityTitle])); + + // When an object has both an accessibilityLabel and an accessibilityTitle, + // VoiceOver prefers the value of accessibilityLabel. Because the native + // window is not an AXPlatformNodeCocoa object, we can set the value of + // accessibilityLabel on the window to the original title ("url.com + // says") via OverrideNativeWindowTitle and VoiceOver presents that + // prior to speaking the RootView's message text. + EXPECT_EQ(title, + base::SysNSStringToUTF16([native_window accessibilityLabel])); +} diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn index 8110d0283b4822..1c1a7e5cdedf58 100644 --- a/chrome/test/BUILD.gn +++ b/chrome/test/BUILD.gn @@ -3308,6 +3308,7 @@ if (!is_android) { } if (is_mac) { sources += [ + "../browser/ui/views/javascript_tab_modal_dialog_view_views_browsertest_mac.mm", "../browser/ui/views/status_bubble_views_browsertest_mac.h", "../browser/ui/views/status_bubble_views_browsertest_mac.mm", ] diff --git a/ui/views/accessibility/view_accessibility.cc b/ui/views/accessibility/view_accessibility.cc index 73a968a3ebd0bc..442c77becc9291 100644 --- a/ui/views/accessibility/view_accessibility.cc +++ b/ui/views/accessibility/view_accessibility.cc @@ -362,6 +362,14 @@ void ViewAccessibility::OverrideDescription(const std::u16string& description) { custom_data_.SetDescription(description); } +void ViewAccessibility::OverrideNativeWindowTitle(const std::string& title) { + NOTIMPLEMENTED() << "Only implemented on Mac for now."; +} + +void ViewAccessibility::OverrideNativeWindowTitle(const std::u16string& title) { + OverrideNativeWindowTitle(base::UTF16ToUTF8(title)); +} + void ViewAccessibility::OverrideIsLeaf(bool value) { is_leaf_ = value; } diff --git a/ui/views/accessibility/view_accessibility.h b/ui/views/accessibility/view_accessibility.h index 4028a007ef0306..9b8cfcf319e67d 100644 --- a/ui/views/accessibility/view_accessibility.h +++ b/ui/views/accessibility/view_accessibility.h @@ -104,6 +104,17 @@ class VIEWS_EXPORT ViewAccessibility { void OverrideDescription(const std::string& description); void OverrideDescription(const std::u16string& description); + // Sets the platform-specific accessible name/title property of the + // NativeViewAccessible window. This is needed on platforms where the name + // of the NativeViewAccessible window is automatically calculated by the + // platform's accessibility API. For instance on the Mac, the label of the + // NativeWidgetMacNSWindow of a JavaScript alert is taken from the name of + // the child RootView. Note: the first function does the string conversion + // and calls the second, thus only the latter needs to be implemented by + // interested platforms. + void OverrideNativeWindowTitle(const std::u16string& title); + virtual void OverrideNativeWindowTitle(const std::string& title); + // Sets whether this View hides all its descendants from the accessibility // tree that is exposed to platform APIs. This is similar, but not exactly // identical to aria-hidden="true". diff --git a/ui/views/accessibility/view_ax_platform_node_delegate_mac.h b/ui/views/accessibility/view_ax_platform_node_delegate_mac.h index ff81be39f2fa68..2a47e1809b8a48 100644 --- a/ui/views/accessibility/view_ax_platform_node_delegate_mac.h +++ b/ui/views/accessibility/view_ax_platform_node_delegate_mac.h @@ -7,6 +7,8 @@ #include "ui/views/accessibility/view_ax_platform_node_delegate.h" +#include + namespace views { // Mac-specific accessibility class for |ViewAXPlatformNodeDelegate|. @@ -21,6 +23,12 @@ class ViewAXPlatformNodeDelegateMac : public ViewAXPlatformNodeDelegate { // |ViewAXPlatformNodeDelegate| overrides: gfx::NativeViewAccessible GetNSWindow() override; gfx::NativeViewAccessible GetParent() const override; + + // |ViewAccessibility| overrides: + void OverrideNativeWindowTitle(const std::string& title) override; + + // |AXPlatformNodeDelegate| overrides: + const std::string& GetName() const override; }; } // namespace views diff --git a/ui/views/accessibility/view_ax_platform_node_delegate_mac.mm b/ui/views/accessibility/view_ax_platform_node_delegate_mac.mm index 37b2b269867a4c..bb82bdf2786d3c 100644 --- a/ui/views/accessibility/view_ax_platform_node_delegate_mac.mm +++ b/ui/views/accessibility/view_ax_platform_node_delegate_mac.mm @@ -10,6 +10,7 @@ #include "ui/views/cocoa/native_widget_mac_ns_window_host.h" #include "ui/views/view.h" #include "ui/views/widget/widget.h" +#include "ui/views/widget/widget_delegate.h" namespace views { @@ -54,4 +55,39 @@ return window_host->GetNativeViewAccessibleForNSView(); } +const std::string& ViewAXPlatformNodeDelegateMac::GetName() const { + // By default, the kDialog name is the title of the window. NSAccessibility + // then applies that name to the native NSWindow. This causes VoiceOver to + // double-speak the name. In the case of some dialogs, such as the one + // associated with a JavaScript alert, we set the accessible description + // to the message contents. For screen readers which prefer the description + // over the displayed text, this causes both the title and message to be + // presented to the user. At the present time, VoiceOver is not one of those + // screen readers. Therefore if we have a dialog whose name is the same as + // the window title, and we also have an explicitly-provided description, set + // the name of the dialog to that description. This causes VoiceOver to read + // both the title and the displayed text. Note that in order for this to + // work, it is necessary for the View to also call OverrideNativeWindowTitle. + // Otherwise, NSAccessibility will set the window title to the message text. + const std::string& name = ViewAXPlatformNodeDelegate::GetName(); + if (!ui::IsDialog(GetRole()) || + !HasStringAttribute(ax::mojom::StringAttribute::kDescription)) + return name; + + if (auto* widget = view()->GetWidget()) { + if (auto* widget_delegate = widget->widget_delegate()) { + if (base::UTF16ToUTF8(widget_delegate->GetWindowTitle()) == name) + return GetStringAttribute(ax::mojom::StringAttribute::kDescription); + } + } + return name; +} + +void ViewAXPlatformNodeDelegateMac::OverrideNativeWindowTitle( + const std::string& title) { + if (gfx::NativeViewAccessible ax_window = GetNSWindow()) { + [ax_window setAccessibilityLabel:base::SysUTF8ToNSString(title)]; + } +} + } // namespace views