Skip to content

Commit

Permalink
Add means to override platform accessibility API native Window title
Browse files Browse the repository at this point in the history
On some platforms, e.g. macOS, the accessible name of the native
window is calculated based on its child (e.g. the RootView). As a
result, in a JavaScript alert dialog, VoiceOver repeats the title
(e.g. "url.com says") and fails to present the message text. If we
set the accessible name of the RootView to the message text, VoiceOver
then repeats that message without speaking the title.

This commit specifically handles this problem by doing the following:

* On macOS, if the RootView is a dialog whose name is the same as
  the title and it has an explicitly-set description, expose that
  description as the RootView's accessible name.
* Create a means for a View to explicitly set the accessible title
  of a native window so that it is not calculated based on the
  name of the RootView. This support is currently only implemented
  for macOS, but it can be implemented on other platforms should
  the need arise.
* Use this override in JavascriptTabModalDialogViewViews, which
  already sets the accessible description of the RootView for
  screen readers such as NVDA and Orca which prefer the description
  over their own calculations.

AX-Relnotes: VoiceOver will now speak the message text of
JavaScript alert dialogs.

Bug: 1199159
Change-Id: I8e3bfe91d701911ecf637b5c9717fc3cbce5d481
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3647873
Reviewed-by: Nektarios Paisios <[email protected]>
Reviewed-by: Elly Fong-Jones <[email protected]>
Commit-Queue: Joanmarie Diggs <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1010303}
  • Loading branch information
joanmarie authored and Chromium LUCI CQ committed Jun 2, 2022
1 parent 0b5da06 commit 6eda4ba
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <string>

#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]));
}
1 change: 1 addition & 0 deletions chrome/test/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
8 changes: 8 additions & 0 deletions ui/views/accessibility/view_accessibility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
11 changes: 11 additions & 0 deletions ui/views/accessibility/view_accessibility.h
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
8 changes: 8 additions & 0 deletions ui/views/accessibility/view_ax_platform_node_delegate_mac.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include "ui/views/accessibility/view_ax_platform_node_delegate.h"

#include <string>

namespace views {

// Mac-specific accessibility class for |ViewAXPlatformNodeDelegate|.
Expand All @@ -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
Expand Down
36 changes: 36 additions & 0 deletions ui/views/accessibility/view_ax_platform_node_delegate_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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

0 comments on commit 6eda4ba

Please sign in to comment.