Commit 6299e21a authored by Aaron Leventhal's avatar Aaron Leventhal Committed by Commit Bot

Prepare for better autofill screen reading experience.

Separate out the different concepts:
- Suggestions are available or not (call ViewAccessibility::OnInputSuggestionsAvailable or
OnInputSuggestionsUnavailable). This is now a static so no view is required in case the menu
for the suggestions has not yet been created.
- Menu is open -- the AX code now just uses the menustart/end event to track this
- Menu item is selected -- the AX code now just uses the selection event on a menuitem when
menus are open

By separating out these concepts, a11y can expose the fake menu item focus only when a menu
item is actually selected, and indicate when a textfield has suggestions available even
if the menu popup is not visible.

Bug: 848427
Change-Id: I4a75c58402d1dd1e5da0368b59f6c9ba7fc8775c
Reviewed-on: https://chromium-review.googlesource.com/1081022
Commit-Queue: Aaron Leventhal <aleventhal@chromium.org>
Reviewed-by: default avatarEvan Stade <estade@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: default avatarFabio Tirelo <ftirelo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#565037}
parent 138a5a2f
......@@ -17,6 +17,7 @@
#include "components/autofill/core/browser/suggestion.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/font.h"
#include "ui/gfx/geometry/rect_conversions.h"
......@@ -417,9 +418,19 @@ gfx::Size AutofillPopupViewNativeViews::CalculatePreferredSize() const {
void AutofillPopupViewNativeViews::VisibilityChanged(View* starting_from,
bool is_visible) {
if (is_visible) {
GetViewAccessibility().OnAutofillShown();
// TODO(https://crbug.com/848427) Call this when suggestions become
// available at all, even if it not currently visible.
ui::AXPlatformNode::OnInputSuggestionsAvailable();
// Fire these the first time a menu is visible. By firing these and the
// matching end events, we are telling screen readers that the focus
// is only changing temporarily, and the screen reader will restore the
// focus back to the appropriate textfield when the menu closes.
NotifyAccessibilityEvent(ax::mojom::Event::kMenuStart, true);
} else {
GetViewAccessibility().OnAutofillHidden();
// TODO(https://crbug.com/848427) Only call if suggestions are actually no
// longer available. The suggestions could be hidden but still available, as
// is the case when the Escape key is pressed.
ui::AXPlatformNode::OnInputSuggestionsUnavailable();
NotifyAccessibilityEvent(ax::mojom::Event::kMenuEnd, true);
}
}
......@@ -429,15 +440,6 @@ void AutofillPopupViewNativeViews::OnSelectedRowChanged(
base::Optional<int> current_row_selection) {
if (previous_row_selection) {
rows_[*previous_row_selection]->SetSelected(false);
} else {
// Fire this the first time a row is selected. By firing this and the
// matching kMenuEnd event, we are telling screen readers that the focus
// is only changing temporarily, and the screen reader will restore the
// focus back to the appropriate textfield when the menu closes.
// This is deferred until the first focus so that the screen reader doesn't
// treat the textfield as unfocused while the user edits, just because
// autofill options are visible.
NotifyAccessibilityEvent(ax::mojom::Event::kMenuStart, true);
}
if (current_row_selection)
......
......@@ -18,6 +18,7 @@
#include "components/autofill/core/browser/popup_item_ids.h"
#include "components/autofill/core/browser/suggestion.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/canvas.h"
......@@ -105,13 +106,18 @@ AutofillPopupViewViews::~AutofillPopupViewViews() {}
void AutofillPopupViewViews::Show() {
DoShow();
GetViewAccessibility().OnAutofillShown();
ui::AXPlatformNode::OnInputSuggestionsAvailable();
// Fire these the first time a menu is visible. By firing these and the
// matching end events, we are telling screen readers that the focus
// is only changing temporarily, and the screen reader will restore the
// focus back to the appropriate textfield when the menu closes.
NotifyAccessibilityEvent(ax::mojom::Event::kMenuStart, true);
}
void AutofillPopupViewViews::Hide() {
// The controller is no longer valid after it hides us.
controller_ = NULL;
GetViewAccessibility().OnAutofillHidden();
ui::AXPlatformNode::OnInputSuggestionsUnavailable();
DoHide();
NotifyAccessibilityEvent(ax::mojom::Event::kMenuEnd, true);
}
......@@ -163,15 +169,6 @@ void AutofillPopupViewViews::OnSelectedRowChanged(
if (previous_row_selection) {
GetChildRow(*previous_row_selection)->OnUnselected();
} else {
// Fire this the first time a row is selected. By firing this and the
// matching kMenuEnd event, we are telling screen readers that the focus
// is only changing temporarily, and the screen reader will restore the
// focus back to the appropriate textfield when the menu closes.
// This is deferred until the first focus so that the screen reader doesn't
// treat the textfield as unfocused while the user edits, just because
// autofill options are visible.
NotifyAccessibilityEvent(ax::mojom::Event::kMenuStart, true);
}
if (current_row_selection) {
AutofillPopupChildView* current_row = GetChildRow(*current_row_selection);
......
......@@ -22,7 +22,10 @@ base::LazyInstance<AXPlatformNode::NativeWindowHandlerCallback>::Leaky
AXPlatformNode::native_window_handler_ = LAZY_INSTANCE_INITIALIZER;
// static
bool AXPlatformNode::is_autofill_shown_ = false;
AXMode AXPlatformNode::ax_mode_;
// static
bool AXPlatformNode::has_input_suggestions_ = false;
// static
AXPlatformNode* AXPlatformNode::FromNativeWindow(
......@@ -71,23 +74,24 @@ void AXPlatformNode::RemoveAXModeObserver(AXModeObserver* observer) {
// static
void AXPlatformNode::NotifyAddAXModeFlags(AXMode mode_flags) {
ax_mode_ |= mode_flags;
for (auto& observer : ax_mode_observers_.Get())
observer.OnAXModeAdded(mode_flags);
}
// static
void AXPlatformNode::OnAutofillShown() {
is_autofill_shown_ = true;
void AXPlatformNode::OnInputSuggestionsAvailable() {
has_input_suggestions_ = true;
}
// static
void AXPlatformNode::OnAutofillHidden() {
is_autofill_shown_ = false;
void AXPlatformNode::OnInputSuggestionsUnavailable() {
has_input_suggestions_ = false;
}
// static
bool AXPlatformNode::IsAutofillShown() {
return is_autofill_shown_;
bool AXPlatformNode::HasInputSuggestions() {
return has_input_suggestions_;
}
} // namespace ui
......@@ -12,6 +12,7 @@
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_export.h"
#include "ui/accessibility/ax_mode_observer.h"
#include "ui/accessibility/ax_modes.h"
#include "ui/gfx/native_widget_types.h"
namespace ui {
......@@ -49,13 +50,25 @@ class AX_EXPORT AXPlatformNode {
static void AddAXModeObserver(AXModeObserver* observer);
static void RemoveAXModeObserver(AXModeObserver* observer);
// Convenience method to get the current accessibility mode.
static AXMode GetAccessibilityMode() { return ax_mode_; }
// Helper static function to notify all global observers about
// the addition of an AXMode flag.
static void NotifyAddAXModeFlags(AXMode mode_flags);
static void OnAutofillShown();
static void OnAutofillHidden();
static bool IsAutofillShown();
// Must be called by native suggestion code when there are suggestions which
// could be presented in a popup, even if the popup is not presently visible.
// The availability of the popup changes the interactions that will occur
// (down arrow will move the focus into the suggestion popup). An example of a
// suggestion popup is seen in the Autofill feature.
static void OnInputSuggestionsAvailable();
// Must be called when the system goes from a state of having an available
// suggestion popup to none available. If the suggestion popup is still
// available but just hidden, this method should not be called.
static void OnInputSuggestionsUnavailable();
static bool HasInputSuggestions();
// Call Destroy rather than deleting this, because the subclass may
// use reference counting.
......@@ -88,7 +101,9 @@ class AX_EXPORT AXPlatformNode {
static base::LazyInstance<NativeWindowHandlerCallback>::Leaky
native_window_handler_;
static bool is_autofill_shown_;
static AXMode ax_mode_;
static bool has_input_suggestions_;
DISALLOW_COPY_AND_ASSIGN(AXPlatformNode);
};
......
......@@ -1251,7 +1251,7 @@ AtkRole AXPlatformNodeAuraLinux::GetAtkRole() {
case ax::mojom::Role::kSearchBox:
if (!GetStringAttribute(ax::mojom::StringAttribute::kAutoComplete)
.empty() ||
IsAutofillField()) {
IsFocusedInputWithSuggestions()) {
return ATK_ROLE_AUTOCOMPLETE;
;
}
......
......@@ -265,8 +265,8 @@ bool AXPlatformNodeBase::IsTextOnlyObject() const {
GetData().role == ax::mojom::Role::kInlineTextBox;
}
bool AXPlatformNodeBase::IsAutofillField() {
return IsAutofillShown() && IsPlainTextField() &&
bool AXPlatformNodeBase::IsFocusedInputWithSuggestions() {
return HasInputSuggestions() && IsPlainTextField() &&
delegate_->GetFocus() == GetNativeViewAccessible();
}
......
......@@ -151,8 +151,11 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode {
bool IsTextOnlyObject() const;
bool IsPlainTextField() const;
// Is in a focused textfield with a related autofill popup currently visible.
bool IsAutofillField();
// Is in a focused textfield with a related suggestion popup available,
// such as for the Autofill feature. The suggestion popup can be either hidden
// and available or already visible. This indicates next down arrow key will
// navigate within the suggestion popup.
bool IsFocusedInputWithSuggestions();
bool IsRichTextField() const;
bool IsRangeValueSupported() const;
......
......@@ -3190,7 +3190,7 @@ int32_t AXPlatformNodeWin::ComputeIA2State() {
}
if (!GetStringAttribute(ax::mojom::StringAttribute::kAutoComplete).empty() ||
IsAutofillField()) {
IsFocusedInputWithSuggestions()) {
ia2_state |= IA2_STATE_SUPPORTS_AUTOCOMPLETION;
}
......@@ -3421,7 +3421,7 @@ std::vector<base::string16> AXPlatformNodeWin::ComputeIA2Attributes() {
StringAttributeToIA2(result, ax::mojom::StringAttribute::kAutoComplete,
"autocomplete");
if (!HasStringAttribute(ax::mojom::StringAttribute::kAutoComplete) &&
IsAutofillField()) {
IsFocusedInputWithSuggestions()) {
result.push_back(L"autocomplete:list");
}
......@@ -3489,9 +3489,9 @@ std::vector<base::string16> AXPlatformNodeWin::ComputeIA2Attributes() {
result.push_back(L"haspopup:dialog");
break;
}
} else if (IsAutofillField()) {
// Note: autofill is special-cased here because there is no way for the
// browser to know when the autofill popup is shown.
} else if (IsFocusedInputWithSuggestions()) {
// Note: suggestions are special-cased here because there is no way
// for the browser to know when a suggestion popup is available.
result.push_back(L"haspopup:menu");
}
......@@ -3837,10 +3837,10 @@ int AXPlatformNodeWin::MSAAState() {
if (ShouldNodeHaveFocusableState(data))
msaa_state |= STATE_SYSTEM_FOCUSABLE;
// Note: autofill is special-cased here because there is no way for the
// browser to know when the autofill popup is shown.
// Note: suggestions are special-cased here because there is no way
// for the browser to know when a suggestion popup is available.
if (data.HasIntAttribute(ax::mojom::IntAttribute::kHasPopup) ||
IsAutofillField())
IsFocusedInputWithSuggestions())
msaa_state |= STATE_SYSTEM_HASPOPUP;
// TODO(dougt) unhandled ux::ax::mojom::State::kHorizontal
......
......@@ -99,6 +99,7 @@ void FlushQueue() {
} // namespace
// static
int NativeViewAccessibilityBase::menu_depth_ = 0;
int32_t NativeViewAccessibilityBase::fake_focus_view_id_ = 0;
NativeViewAccessibilityBase::NativeViewAccessibilityBase(View* view)
......@@ -134,17 +135,53 @@ void NativeViewAccessibilityBase::NotifyAccessibilityEvent(
ax_node_->NotifyAccessibilityEvent(event_type);
// A focus context event is intended to send a focus event and a delay
// before the next focus event. It makes sense to delay the entire next
// synchronous batch of next events so that ordering remains the same.
if (event_type == ax::mojom::Event::kFocusContext) {
// Begin queueing subsequent events and flush queue asynchronously.
g_is_queueing_events = true;
base::OnceCallback<void()> cb = base::BindOnce(&FlushQueue);
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(cb));
// Some events have special handling.
switch (event_type) {
case ax::mojom::Event::kMenuStart:
OnMenuStart();
break;
case ax::mojom::Event::kMenuEnd:
OnMenuEnd();
break;
case ax::mojom::Event::kSelection:
if (menu_depth_ && GetData().role == ax::mojom::Role::kMenuItem)
OnMenuItemActive();
break;
case ax::mojom::Event::kFocusContext: {
// A focus context event is intended to send a focus event and a delay
// before the next focus event. It makes sense to delay the entire next
// synchronous batch of next events so that ordering remains the same.
// Begin queueing subsequent events and flush queue asynchronously.
g_is_queueing_events = true;
base::OnceCallback<void()> cb = base::BindOnce(&FlushQueue);
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(cb));
break;
}
default:
break;
}
}
void NativeViewAccessibilityBase::OnMenuItemActive() {
// When a native menu is shown and has an item selected, treat it and the
// currently selected item as focused, even though the actual focus is in the
// browser's currently focused textfield.
fake_focus_view_id_ = GetUniqueId().Get();
}
void NativeViewAccessibilityBase::OnMenuStart() {
++menu_depth_;
}
void NativeViewAccessibilityBase::OnMenuEnd() {
// When a native menu is hidden, restore accessibility focus to the current
// focus in the document.
DCHECK_GE(menu_depth_, 1);
--menu_depth_;
if (menu_depth_ == 0)
fake_focus_view_id_ = 0;
}
// ui::AXPlatformNodeDelegate
const ui::AXNodeData& NativeViewAccessibilityBase::GetData() const {
......@@ -281,23 +318,6 @@ gfx::NativeViewAccessible NativeViewAccessibilityBase::HitTestSync(int x,
return GetNativeObject();
}
void NativeViewAccessibilityBase::OnAutofillShown() {
// When the autofill is shown, treat it and the currently selected item as
// focused, even though the actual focus is in the browser's currently
// focused textfield.
DCHECK(!fake_focus_view_id_) << "Cannot have more that one fake focus.";
fake_focus_view_id_ = GetUniqueId().Get();
ui::AXPlatformNode::OnAutofillShown();
}
void NativeViewAccessibilityBase::OnAutofillHidden() {
DCHECK(fake_focus_view_id_) << "No autofill fake focus set.";
DCHECK_EQ(fake_focus_view_id_, GetUniqueId().Get())
<< "Cannot clear autofill fake focus on an object that did not have it.";
fake_focus_view_id_ = 0;
ui::AXPlatformNode::OnAutofillHidden();
}
gfx::NativeViewAccessible NativeViewAccessibilityBase::GetFocus() {
FocusManager* focus_manager = view()->GetFocusManager();
View* focused_view =
......
......@@ -37,8 +37,6 @@ class VIEWS_EXPORT NativeViewAccessibilityBase
// ViewAccessibility:
gfx::NativeViewAccessible GetNativeObject() override;
void NotifyAccessibilityEvent(ax::mojom::Event event_type) override;
void OnAutofillShown() override;
void OnAutofillHidden() override;
// ui::AXPlatformNodeDelegate
const ui::AXNodeData& GetData() const override;
......@@ -77,13 +75,20 @@ class VIEWS_EXPORT NativeViewAccessibilityBase
private:
void PopulateChildWidgetVector(std::vector<Widget*>* result_child_widgets);
void OnMenuItemActive();
void OnMenuStart();
void OnMenuEnd();
// We own this, but it is reference-counted on some platforms so we can't use
// a scoped_ptr. It is dereferenced in the destructor.
ui::AXPlatformNode* ax_node_;
mutable ui::AXNodeData data_;
// This allows UI popups like autofill to act as if they are focused in the
// Levels of menu are currently open, e.g. 0: none, 1: top, 2: submenu ...
static int32_t menu_depth_;
// This allows UI menu popups like to act as if they are focused in the
// exposed platform accessibility API, even though true focus remains in
// underlying content.
static int32_t fake_focus_view_id_;
......
......@@ -5,6 +5,7 @@
#include "ui/views/accessibility/view_accessibility.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#include "ui/base/ui_features.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
......
......@@ -51,9 +51,6 @@ class VIEWS_EXPORT ViewAccessibility {
void OverrideDescription(const std::string& description);
void OverrideIsLeaf(); // Force this node to be treated as a leaf node.
virtual void OnAutofillShown(){};
virtual void OnAutofillHidden(){};
virtual gfx::NativeViewAccessible GetNativeObject();
virtual void NotifyAccessibilityEvent(ax::mojom::Event event_type) {}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment