diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/ui/accessibility | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-85-based.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/ui/accessibility')
65 files changed, 2863 insertions, 748 deletions
diff --git a/chromium/ui/accessibility/BUILD.gn b/chromium/ui/accessibility/BUILD.gn index fc74899db46..0297bff6f13 100644 --- a/chromium/ui/accessibility/BUILD.gn +++ b/chromium/ui/accessibility/BUILD.gn @@ -16,6 +16,10 @@ if (is_android) { import("//build/config/android/rules.gni") } +if (is_win) { + import("//build/toolchain/win/midl.gni") +} + # Reset sources_assignment_filter for the BUILD.gn file to prevent # regression during the migration of Chromium away from the feature. # See docs/no_sources_assignment_filter.md for more information. @@ -41,6 +45,10 @@ jumbo_component("ax_base") { defines = [ "AX_BASE_IMPLEMENTATION" ] sources = [ + "accessibility_features.cc", + "accessibility_features.h", + "accessibility_switches.cc", + "accessibility_switches.h", "ax_base_export.h", "ax_enum_util.cc", "ax_enum_util.h", @@ -70,14 +78,18 @@ jumbo_component("ax_base") { ] } +#if (is_win) { +# midl("ichromeaccessible") { +# sources = [ +# "platform/ichromeaccessible.idl", +# ] +# } +#} + jumbo_component("accessibility") { defines = [ "AX_IMPLEMENTATION" ] sources = [ - "accessibility_features.cc", - "accessibility_features.h", - "accessibility_switches.cc", - "accessibility_switches.h", "ax_action_data.cc", "ax_action_data.h", "ax_action_handler.cc", @@ -132,85 +144,21 @@ jumbo_component("accessibility") { "ax_tree_update_forward.h", "null_ax_action_target.cc", "null_ax_action_target.h", - - # ax_android_constants* are used in ax_assistant_structure.cc. - "platform/ax_android_constants.cc", - "platform/ax_android_constants.h", - - # ax_platform_node* are used for enable/disable accessibility and - # test_ax_node_wrapper. - "platform/ax_platform_node.cc", - "platform/ax_platform_node.h", - "platform/ax_platform_node_base.cc", - "platform/ax_platform_node_base.h", - "platform/ax_platform_node_delegate.h", - "platform/ax_platform_node_delegate_base.cc", - "platform/ax_platform_node_delegate_base.h", - - # ax_platform_node_test_helper.{cc,h} are used in - # browser_view_browsertest.cc - "platform/ax_platform_node_test_helper.cc", - "platform/ax_platform_node_test_helper.h", - - # ax_unique_id.{cc,h} are used in browser_accessibility.cc and - # view_accessibility.cc - "platform/ax_unique_id.cc", - "platform/ax_unique_id.h", - - # compute_attributes.{cc,h} are used in - # accessibility_tree_formatter_blink.cc - "platform/compute_attributes.cc", - "platform/compute_attributes.h", ] - deps = [ "//third_party/cld_3/src/src:cld_3" ] + deps = [ + "//base/util/values:values_util", + "//third_party/cld_3/src/src:cld_3", + ] public_deps = [ ":ax_base", - "//ui/display", + "//ui/accessibility/platform", ] - if (has_native_accessibility) { - sources += [ - "platform/ax_platform_text_boundary.cc", - "platform/ax_platform_text_boundary.h", - ] - - if (use_atk) { - # ax_platform_text_boundary.h includes atk.h, so ATK is needed as a public - # config to ensure anything that includes this is able to find atk.h. - public_configs = [ "//build/config/linux/atk" ] - } - } - - if (is_win) { - sources += [ - "platform/ax_fragment_root_delegate_win.h", - "platform/ax_fragment_root_win.cc", - "platform/ax_fragment_root_win.h", - "platform/ax_platform_node_delegate_utils_win.cc", - "platform/ax_platform_node_delegate_utils_win.h", - "platform/ax_platform_node_textchildprovider_win.cc", - "platform/ax_platform_node_textchildprovider_win.h", - "platform/ax_platform_node_textprovider_win.cc", - "platform/ax_platform_node_textprovider_win.h", - "platform/ax_platform_node_textrangeprovider_win.cc", - "platform/ax_platform_node_textrangeprovider_win.h", - "platform/ax_platform_node_win.cc", - "platform/ax_platform_node_win.h", - "platform/ax_platform_relation_win.cc", - "platform/ax_platform_relation_win.h", - "platform/ax_system_caret_win.cc", - "platform/ax_system_caret_win.h", - ] - - public_deps += [ "//third_party/iaccessible2" ] - - libs = [ - "oleacc.lib", - "uiautomationcore.lib", - ] - } + # Allows the files from //ui/accessibility/platform includes headers + # from this directory. + allow_circular_includes_from = [ "//ui/accessibility/platform" ] if (!is_ios) { sources += [ @@ -225,40 +173,6 @@ jumbo_component("accessibility") { ] } - if (is_mac) { - sources += [ - "platform/ax_platform_node_mac.h", - "platform/ax_platform_node_mac.mm", - ] - - libs = [ - "AppKit.framework", - "Foundation.framework", - ] - } - - if (use_atk) { - sources += [ - "platform/atk_util_auralinux.cc", - "platform/atk_util_auralinux.h", - "platform/atk_util_auralinux_gtk.cc", - "platform/ax_platform_atk_hyperlink.cc", - "platform/ax_platform_atk_hyperlink.h", - "platform/ax_platform_node_auralinux.cc", - "platform/ax_platform_node_auralinux.h", - ] - - configs += [ "//build/config/linux/atk" ] - - if (use_glib) { - configs += [ "//build/config/linux:glib" ] - } - - if (use_x11) { - public_deps += [ "//ui/gfx/x" ] - } - } - if (use_aura) { sources += [ "aura/aura_window_properties.cc", @@ -281,15 +195,21 @@ source_set("ax_assistant") { static_library("test_support") { testonly = true sources = [ - # test_ax_node_wrapper.{cc,h} are used in ax_range_unittest.cc - "platform/test_ax_node_wrapper.cc", - "platform/test_ax_node_wrapper.h", + "test_ax_node_helper.cc", + "test_ax_node_helper.h", "test_ax_tree_manager.cc", "test_ax_tree_manager.h", "tree_generator.cc", "tree_generator.h", ] + if (has_native_accessibility) { + sources += [ + "platform/test_ax_node_wrapper.cc", + "platform/test_ax_node_wrapper.h", + ] + } + deps = [ ":accessibility" ] } @@ -325,23 +245,6 @@ test("accessibility_unittests") { "run_all_unittests.cc", ] - if (is_win) { - sources += [ - "platform/ax_fragment_root_win_unittest.cc", - "platform/ax_platform_node_textchildprovider_win_unittest.cc", - "platform/ax_platform_node_textprovider_win_unittest.cc", - "platform/ax_platform_node_textrangeprovider_win_unittest.cc", - "platform/ax_platform_node_win_unittest.cc", - "platform/ax_platform_node_win_unittest.h", - ] - } - - if (has_native_accessibility) { - # This test depends heavily on NativeViewAccessible, which is only - # implemented on these platforms. - sources += [ "platform/ax_platform_node_base_unittest.cc" ] - } - deps = [ ":accessibility", ":test_support", @@ -357,22 +260,40 @@ test("accessibility_unittests") { "//ui/gfx:test_support", ] - if (is_win) { - deps += [ "//third_party/iaccessible2" ] + if (has_native_accessibility) { + # This test depends heavily on NativeViewAccessible, which is only + # implemented on these platforms. + sources += [ "platform/ax_platform_node_base_unittest.cc" ] - libs = [ - "oleacc.lib", - "uiautomationcore.lib", - ] - } + if (is_win) { + sources += [ + "platform/ax_fragment_root_win_unittest.cc", + "platform/ax_platform_node_textchildprovider_win_unittest.cc", + "platform/ax_platform_node_textprovider_win_unittest.cc", + "platform/ax_platform_node_textrangeprovider_win_unittest.cc", + "platform/ax_platform_node_win_unittest.cc", + "platform/ax_platform_node_win_unittest.h", + ] + + deps += [ + "//third_party/iaccessible2", + "//ui/accessibility/platform:ichromeaccessible", + ] + + libs = [ + "oleacc.lib", + "uiautomationcore.lib", + ] + } - if (use_atk) { - sources += [ - "platform/atk_util_auralinux_unittest.cc", - "platform/ax_platform_node_auralinux_unittest.cc", - ] + if (use_atk) { + sources += [ + "platform/atk_util_auralinux_unittest.cc", + "platform/ax_platform_node_auralinux_unittest.cc", + ] - configs += [ "//build/config/linux/atk" ] + configs += [ "//build/config/linux/atk" ] + } } } diff --git a/chromium/ui/accessibility/accessibility_features.cc b/chromium/ui/accessibility/accessibility_features.cc index 6b6f63f1b90..4a52188c1b6 100644 --- a/chromium/ui/accessibility/accessibility_features.cc +++ b/chromium/ui/accessibility/accessibility_features.cc @@ -47,4 +47,36 @@ bool IsAccessibilityTreeForViewsEnabled() { ::features::kEnableAccessibilityTreeForViews); } +const base::Feature kAccessibilityFocusHighlight{ + "AccessibilityFocusHighlight", base::FEATURE_DISABLED_BY_DEFAULT}; + +bool IsAccessibilityFocusHighlightEnabled() { + return base::FeatureList::IsEnabled(::features::kAccessibilityFocusHighlight); +} + +#if defined(OS_WIN) +const base::Feature kIChromeAccessible{"IChromeAccessible", + base::FEATURE_DISABLED_BY_DEFAULT}; + +bool IsIChromeAccessibleEnabled() { + return base::FeatureList::IsEnabled(::features::kIChromeAccessible); +} +#endif // defined(OS_WIN) + +#if defined(OS_CHROMEOS) +const base::Feature kAccessibilityCursorColor{ + "AccessibilityCursorColor", base::FEATURE_DISABLED_BY_DEFAULT}; + +bool IsAccessibilityCursorColorEnabled() { + return base::FeatureList::IsEnabled(::features::kAccessibilityCursorColor); +} +#endif // defined(OS_CHROMEOS) + +const base::Feature kAugmentExistingImageLabels{ + "AugmentExistingImageLabels", base::FEATURE_DISABLED_BY_DEFAULT}; + +bool IsAugmentExistingImageLabelsEnabled() { + return base::FeatureList::IsEnabled(::features::kAugmentExistingImageLabels); +} + } // namespace features diff --git a/chromium/ui/accessibility/accessibility_features.h b/chromium/ui/accessibility/accessibility_features.h index b7fa71ec600..743a32c5bc7 100644 --- a/chromium/ui/accessibility/accessibility_features.h +++ b/chromium/ui/accessibility/accessibility_features.h @@ -8,35 +8,67 @@ #include "base/feature_list.h" #include "build/build_config.h" -#include "ui/accessibility/ax_export.h" +#include "ui/accessibility/ax_base_export.h" namespace features { -AX_EXPORT extern const base::Feature kEnableAccessibilityExposeARIAAnnotations; +AX_BASE_EXPORT extern const base::Feature + kEnableAccessibilityExposeARIAAnnotations; // Returns true if ARIA annotations should be exposed to the browser AX Tree. -AX_EXPORT bool IsAccessibilityExposeARIAAnnotationsEnabled(); +AX_BASE_EXPORT bool IsAccessibilityExposeARIAAnnotationsEnabled(); -AX_EXPORT extern const base::Feature kEnableAccessibilityExposeDisplayNone; +AX_BASE_EXPORT extern const base::Feature kEnableAccessibilityExposeDisplayNone; // Returns true if "display: none" nodes should be exposed to the // browser process AXTree. -AX_EXPORT bool IsAccessibilityExposeDisplayNoneEnabled(); +AX_BASE_EXPORT bool IsAccessibilityExposeDisplayNoneEnabled(); -AX_EXPORT extern const base::Feature kEnableAccessibilityExposeHTMLElement; +AX_BASE_EXPORT extern const base::Feature kEnableAccessibilityExposeHTMLElement; // Returns true if the <html> element should be exposed to the // browser process AXTree (as an ignored node). -AX_EXPORT bool IsAccessibilityExposeHTMLElementEnabled(); +AX_BASE_EXPORT bool IsAccessibilityExposeHTMLElementEnabled(); // Serializes accessibility information from the Views tree and deserializes it // into an AXTree in the browser process. -AX_EXPORT extern const base::Feature kEnableAccessibilityTreeForViews; +AX_BASE_EXPORT extern const base::Feature kEnableAccessibilityTreeForViews; // Returns true if the Views tree is exposed using an AXTree in the browser // process. Returns false if the Views tree is exposed to accessibility // directly. -AX_EXPORT bool IsAccessibilityTreeForViewsEnabled(); +AX_BASE_EXPORT bool IsAccessibilityTreeForViewsEnabled(); + +AX_BASE_EXPORT extern const base::Feature kAccessibilityFocusHighlight; + +// Returns true if the accessibility focus highlight feature is enabled, +// which draws a visual highlight around the focused element on the page +// briefly whenever focus changes. +AX_BASE_EXPORT bool IsAccessibilityFocusHighlightEnabled(); + +#if defined(OS_WIN) +// Enables an experimental Chrome-specific accessibility COM API +AX_BASE_EXPORT extern const base::Feature kIChromeAccessible; + +// Returns true if the IChromeAccessible COM API is enabled. +AX_BASE_EXPORT bool IsIChromeAccessibleEnabled(); + +#endif // defined(OS_WIN) + +#if defined(OS_CHROMEOS) +AX_BASE_EXPORT extern const base::Feature kAccessibilityCursorColor; + +// Returns true if the accessibility cursor color feature is enabled, letting +// users pick a custom cursor color. +AX_BASE_EXPORT bool IsAccessibilityCursorColorEnabled(); +#endif // defined(OS_CHROMEOS) + +// Enables Get Image Descriptions to augment existing images labels, +// rather than only provide descriptions for completely unlabeled images. +AX_BASE_EXPORT extern const base::Feature kAugmentExistingImageLabels; + +// Returns true if augmenting existing image labels is enabled. +AX_BASE_EXPORT bool IsAugmentExistingImageLabelsEnabled(); } // namespace features diff --git a/chromium/ui/accessibility/accessibility_switches.h b/chromium/ui/accessibility/accessibility_switches.h index 3cc38c37f10..2d3b9f2761b 100644 --- a/chromium/ui/accessibility/accessibility_switches.h +++ b/chromium/ui/accessibility/accessibility_switches.h @@ -7,41 +7,46 @@ #define UI_ACCESSIBILITY_ACCESSIBILITY_SWITCHES_H_ #include "build/build_config.h" -#include "ui/accessibility/ax_export.h" +#include "ui/accessibility/ax_base_export.h" namespace switches { -AX_EXPORT extern const char kEnableExperimentalAccessibilityAutoclick[]; -AX_EXPORT extern const char kEnableExperimentalAccessibilityLabelsDebugging[]; -AX_EXPORT extern const char kEnableExperimentalAccessibilityLanguageDetection[]; -AX_EXPORT extern const char +AX_BASE_EXPORT extern const char kEnableExperimentalAccessibilityAutoclick[]; +AX_BASE_EXPORT extern const char + kEnableExperimentalAccessibilityLabelsDebugging[]; +AX_BASE_EXPORT extern const char + kEnableExperimentalAccessibilityLanguageDetection[]; +AX_BASE_EXPORT extern const char kEnableExperimentalAccessibilityLanguageDetectionDynamic[]; -AX_EXPORT extern const char kEnableExperimentalAccessibilitySwitchAccess[]; -AX_EXPORT extern const char kEnableExperimentalAccessibilitySwitchAccessText[]; -AX_EXPORT extern const char +AX_BASE_EXPORT extern const char kEnableExperimentalAccessibilitySwitchAccess[]; +AX_BASE_EXPORT extern const char + kEnableExperimentalAccessibilitySwitchAccessText[]; +AX_BASE_EXPORT extern const char kEnableExperimentalAccessibilityChromeVoxAnnotations[]; -AX_EXPORT extern const char +AX_BASE_EXPORT extern const char kDisableExperimentalAccessibilityChromeVoxLanguageSwitching[]; -AX_EXPORT extern const char +AX_BASE_EXPORT extern const char kDisableExperimentalAccessibilityChromeVoxSearchMenus[]; -AX_EXPORT extern const char kEnableExperimentalAccessibilityChromeVoxTutorial[]; +AX_BASE_EXPORT extern const char + kEnableExperimentalAccessibilityChromeVoxTutorial[]; // Returns true if experimental accessibility language detection is enabled. -AX_EXPORT bool IsExperimentalAccessibilityLanguageDetectionEnabled(); +AX_BASE_EXPORT bool IsExperimentalAccessibilityLanguageDetectionEnabled(); // Returns true if experimental accessibility language detection support for // dynamic content is enabled. -AX_EXPORT bool IsExperimentalAccessibilityLanguageDetectionDynamicEnabled(); +AX_BASE_EXPORT bool +IsExperimentalAccessibilityLanguageDetectionDynamicEnabled(); // Returns true if experimental accessibility Switch Access text is enabled. -AX_EXPORT bool IsExperimentalAccessibilitySwitchAccessTextEnabled(); +AX_BASE_EXPORT bool IsExperimentalAccessibilitySwitchAccessTextEnabled(); #if defined(OS_WIN) -AX_EXPORT extern const char kEnableExperimentalUIAutomation[]; +AX_BASE_EXPORT extern const char kEnableExperimentalUIAutomation[]; #endif // Returns true if experimental support for UIAutomation is enabled. -AX_EXPORT bool IsExperimentalAccessibilityPlatformUIAEnabled(); +AX_BASE_EXPORT bool IsExperimentalAccessibilityPlatformUIAEnabled(); } // namespace switches diff --git a/chromium/ui/accessibility/ax_enum_util.cc b/chromium/ui/accessibility/ax_enum_util.cc index 35bbef5afc6..6d592227fc2 100644 --- a/chromium/ui/accessibility/ax_enum_util.cc +++ b/chromium/ui/accessibility/ax_enum_util.cc @@ -81,8 +81,6 @@ const char* ToString(ax::mojom::Event event) { return "menuListValueChanged"; case ax::mojom::Event::kMenuPopupEnd: return "menuPopupEnd"; - case ax::mojom::Event::kMenuPopupHide: - return "menuPopupHide"; case ax::mojom::Event::kMenuPopupStart: return "menuPopupStart"; case ax::mojom::Event::kMenuStart: @@ -211,8 +209,6 @@ ax::mojom::Event ParseEvent(const char* event) { return ax::mojom::Event::kMenuListValueChanged; if (0 == strcmp(event, "menuPopupEnd")) return ax::mojom::Event::kMenuPopupEnd; - if (0 == strcmp(event, "menuPopupHide")) - return ax::mojom::Event::kMenuPopupHide; if (0 == strcmp(event, "menuPopupStart")) return ax::mojom::Event::kMenuPopupStart; if (0 == strcmp(event, "menuStart")) @@ -1884,6 +1880,8 @@ const char* ToString(ax::mojom::BoolAttribute bool_attribute) { return "clipsChildren"; case ax::mojom::BoolAttribute::kSelected: return "selected"; + case ax::mojom::BoolAttribute::kSelectedFromFocus: + return "selectedFromFocus"; case ax::mojom::BoolAttribute::kSupportsTextLocation: return "supportsTextLocation"; case ax::mojom::BoolAttribute::kIsLineBreakingObject: @@ -1926,6 +1924,8 @@ ax::mojom::BoolAttribute ParseBoolAttribute(const char* bool_attribute) { return ax::mojom::BoolAttribute::kClipsChildren; if (0 == strcmp(bool_attribute, "selected")) return ax::mojom::BoolAttribute::kSelected; + if (0 == strcmp(bool_attribute, "selectedFromFocus")) + return ax::mojom::BoolAttribute::kSelectedFromFocus; if (0 == strcmp(bool_attribute, "supportsTextLocation")) return ax::mojom::BoolAttribute::kSupportsTextLocation; if (0 == strcmp(bool_attribute, "isLineBreakingObject")) diff --git a/chromium/ui/accessibility/ax_enums.mojom b/chromium/ui/accessibility/ax_enums.mojom index a5c0fc5884f..b1523555d3b 100644 --- a/chromium/ui/accessibility/ax_enums.mojom +++ b/chromium/ui/accessibility/ax_enums.mojom @@ -27,7 +27,7 @@ module ax.mojom; enum Event { kNone, - kActiveDescendantChanged, // Web + kActiveDescendantChanged, kAlert, kAriaAttributeChanged, // Implicit kAutocorrectionOccured, // Unknown: http://crbug.com/392498 @@ -56,13 +56,12 @@ enum Event { kLocationChanged, // Web kMediaStartedPlaying, // Native / Automation kMediaStoppedPlaying, // Native / Automation - kMenuEnd, // Native / Win + kMenuEnd, // Native / web: menu interaction has ended. kMenuListItemSelected, // Web kMenuListValueChanged, // Web - kMenuPopupEnd, // Native - kMenuPopupHide, // Native / AuraLinux - kMenuPopupStart, // Native - kMenuStart, // Native / Win + kMenuPopupEnd, // Native / web: a menu/submenu is hidden/closed. + kMenuPopupStart, // Native / web: a menu/submenu is shown/opened. + kMenuStart, // Native / web: menu interaction has begun. kMouseCanceled, kMouseDragged, kMouseMoved, @@ -77,7 +76,7 @@ enum Event { kSelection, // Native kSelectionAdd, // Native kSelectionRemove, // Native - kShow, // Remove: http://crbug.com/392502 + kShow, // Native / Automation kStateChanged, // Native / Automation kTextChanged, kWindowActivated, // Native @@ -734,6 +733,9 @@ enum BoolAttribute { // Indicates whether this node is selected or unselected. kSelected, + // Indicates whether this node is selected due to selection follows focus. + kSelectedFromFocus, + // Indicates whether this node supports text location. kSupportsTextLocation, @@ -984,14 +986,14 @@ enum SortDirection { enum NameFrom { kNone, kUninitialized, - kAttribute, + kAttribute, // E.g. aria-label. kAttributeExplicitlyEmpty, - kCaption, + kCaption, // E.g. in the case of a table, from a caption element. kContents, - kPlaceholder, - kRelatedElement, - kTitle, - kValue, + kPlaceholder, // E.g. from an HTML placeholder attribute on a text field. + kRelatedElement, // E.g. from a figcaption Element in a figure. + kTitle, // E.g. <input type="text" title="title">. + kValue, // E.g. <input type="button" value="Button's name">. }; enum DescriptionFrom { diff --git a/chromium/ui/accessibility/ax_event.cc b/chromium/ui/accessibility/ax_event.cc index 34327f466f1..ecb80d908e4 100644 --- a/chromium/ui/accessibility/ax_event.cc +++ b/chromium/ui/accessibility/ax_event.cc @@ -29,7 +29,7 @@ AXEvent::AXEvent(const AXEvent& event) = default; AXEvent& AXEvent::operator=(const AXEvent& event) = default; std::string AXEvent::ToString() const { - std::string result = "AXEvent"; + std::string result = "AXEvent "; result += ui::ToString(event_type); result += " on node id=" + base::NumberToString(id); diff --git a/chromium/ui/accessibility/ax_event_generator.cc b/chromium/ui/accessibility/ax_event_generator.cc index 472dd5b1579..7561e537be6 100644 --- a/chromium/ui/accessibility/ax_event_generator.cc +++ b/chromium/ui/accessibility/ax_event_generator.cc @@ -49,9 +49,13 @@ void RemoveEvent(std::set<AXEventGenerator::EventParams>* node_events, } // namespace -AXEventGenerator::EventParams::EventParams(Event event, - ax::mojom::EventFrom event_from) - : event(event), event_from(event_from) {} +AXEventGenerator::EventParams::EventParams( + Event event, + ax::mojom::EventFrom event_from, + const std::vector<AXEventIntent>& event_intents) + : event(event), event_from(event_from), event_intents(event_intents) {} + +AXEventGenerator::EventParams::~EventParams() = default; AXEventGenerator::TargetedEvent::TargetedEvent(AXNode* node, const EventParams& event_params) @@ -138,7 +142,8 @@ void AXEventGenerator::AddEvent(AXNode* node, AXEventGenerator::Event event) { return; std::set<EventParams>& node_events = tree_events_[node]; - node_events.emplace(event, ax::mojom::EventFrom::kNone); + node_events.emplace(event, ax::mojom::EventFrom::kNone, + tree_->event_intents()); } void AXEventGenerator::OnNodeDataChanged(AXTree* tree, @@ -154,7 +159,8 @@ void AXEventGenerator::OnNodeDataChanged(AXTree* tree, new_node_data.role != ax::mojom::Role::kStaticText) { AXNode* node = tree_->GetFromId(new_node_data.id); tree_events_[node].emplace(Event::CHILDREN_CHANGED, - ax::mojom::EventFrom::kNone); + ax::mojom::EventFrom::kNone, + tree_->event_intents()); } } @@ -543,7 +549,7 @@ void AXEventGenerator::FireLiveRegionEvents(AXNode* node) { .GetStringAttribute(ax::mojom::StringAttribute::kName) .empty()) AddEvent(node, Event::LIVE_REGION_NODE_CHANGED); - // Fire LIVE_REGION_CHANGED on the root of the live region. + // Fire LIVE_REGION_NODE_CHANGED on the root of the live region. AddEvent(live_root, Event::LIVE_REGION_CHANGED); } } @@ -610,6 +616,9 @@ void AXEventGenerator::FireRelationSourceEvents(AXTree* tree, // Attempts to suppress load-related events that we presume no AT will be // interested in under any circumstances, such as pages which have no size. bool AXEventGenerator::ShouldFireLoadEvents(AXNode* node) { + if (always_fire_load_complete_) + return true; + const AXNodeData& data = node->data(); return data.relative_bounds.bounds.width() || data.relative_bounds.bounds.height(); diff --git a/chromium/ui/accessibility/ax_event_generator.h b/chromium/ui/accessibility/ax_event_generator.h index 61d860b1e64..47c64ef7b80 100644 --- a/chromium/ui/accessibility/ax_event_generator.h +++ b/chromium/ui/accessibility/ax_event_generator.h @@ -11,6 +11,7 @@ #include <vector> #include "base/scoped_observer.h" +#include "ui/accessibility/ax_event_intent.h" #include "ui/accessibility/ax_export.h" #include "ui/accessibility/ax_tree.h" #include "ui/accessibility/ax_tree_observer.h" @@ -90,9 +91,13 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { }; struct EventParams { - EventParams(Event event, ax::mojom::EventFrom event_from); + EventParams(Event event, + ax::mojom::EventFrom event_from, + const std::vector<AXEventIntent>& event_intents); + ~EventParams(); Event event; ax::mojom::EventFrom event_from; + std::vector<AXEventIntent> event_intents; bool operator==(const EventParams& rhs); bool operator<(const EventParams& rhs) const; @@ -165,6 +170,10 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { // same order they were added. void AddEvent(ui::AXNode* node, Event event); + void set_always_fire_load_complete(bool val) { + always_fire_load_complete_ = val; + } + protected: // AXTreeObserver overrides. void OnNodeDataChanged(AXTree* tree, @@ -236,6 +245,8 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { // OnAtomicUpdateFinished. List of nodes whose active descendant changed. std::vector<AXNode*> active_descendant_changed_; + bool always_fire_load_complete_ = false; + // Please make sure that this ScopedObserver is always declared last in order // to prevent any use-after-free. ScopedObserver<AXTree, AXTreeObserver> tree_event_observer_{this}; diff --git a/chromium/ui/accessibility/ax_mode.cc b/chromium/ui/accessibility/ax_mode.cc index 55d0fc130db..7919464eb0a 100644 --- a/chromium/ui/accessibility/ax_mode.cc +++ b/chromium/ui/accessibility/ax_mode.cc @@ -41,6 +41,9 @@ std::string AXMode::ToString() const { case AXMode::kLabelImages: flag_name = "kLabelImages"; break; + case AXMode::kPDF: + flag_name = "kPDF"; + break; } DCHECK(flag_name); diff --git a/chromium/ui/accessibility/ax_mode.h b/chromium/ui/accessibility/ax_mode.h index 8e0b2dc5fc0..2536ad56236 100644 --- a/chromium/ui/accessibility/ax_mode.h +++ b/chromium/ui/accessibility/ax_mode.h @@ -10,7 +10,6 @@ #include <ostream> #include <string> -#include "base/logging.h" #include "ui/accessibility/ax_export.h" namespace ui { @@ -60,10 +59,14 @@ class AX_EXPORT AXMode { // The accessibility tree will contain automatic image annotations. static constexpr uint32_t kLabelImages = 1 << 5; + // The accessibility tree will contain enough information to export + // an accessible PDF. + static constexpr uint32_t kPDF = 1 << 6; + // Update this to include the last supported mode flag. If you add // another, be sure to update the stream insertion operator for // logging and debugging. - static constexpr uint32_t kLastModeFlag = 1 << 5; + static constexpr uint32_t kLastModeFlag = 1 << 6; constexpr AXMode() : flags_(0) {} constexpr AXMode(uint32_t flags) : flags_(flags) {} diff --git a/chromium/ui/accessibility/ax_node.cc b/chromium/ui/accessibility/ax_node.cc index b81f1491c31..3c7b06ecd91 100644 --- a/chromium/ui/accessibility/ax_node.cc +++ b/chromium/ui/accessibility/ax_node.cc @@ -10,6 +10,7 @@ #include "base/strings/string16.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_language_detection.h" #include "ui/accessibility/ax_role_properties.h" @@ -36,6 +37,7 @@ AXNode::AXNode(AXNode::OwnerTree* tree, AXNode::~AXNode() = default; size_t AXNode::GetUnignoredChildCount() const { + // TODO(nektar): Should DCHECK if the node is not ignored. DCHECK(!tree_->GetTreeUpdateInProgressState()); return unignored_child_count_; } @@ -320,7 +322,7 @@ AXNode::UnignoredChildIterator AXNode::UnignoredChildrenEnd() const { // The first (direct) child, ignored or unignored. AXNode* AXNode::GetFirstChild() const { - if (children().size() == 0) + if (children().empty()) return nullptr; return children()[0]; } @@ -354,9 +356,13 @@ AXNode* AXNode::GetNextSibling() const { } bool AXNode::IsText() const { - return data().role == ax::mojom::Role::kStaticText || - data().role == ax::mojom::Role::kLineBreak || - data().role == ax::mojom::Role::kInlineTextBox; + // In Legacy Layout, a list marker has no children and is thus represented on + // all platforms as a leaf node that exposes the marker itself, i.e., it forms + // part of the AX tree's text representation. In contrast, in Layout NG, a + // list marker has a static text child. + if (data().role == ax::mojom::Role::kListMarker) + return !children().size(); + return ui::IsText(data().role); } bool AXNode::IsLineBreak() const { @@ -477,6 +483,61 @@ void AXNode::ClearLanguageInfo() { language_info_.reset(); } +std::string AXNode::GetInnerText() const { + // If a text field has no descendants, then we compute its inner text from its + // value or its placeholder. Otherwise we prefer to look at its descendant + // text nodes because Blink doesn't always add all trailing white space to the + // value attribute. + if (data().IsTextField() && children().empty()) { + std::string value = + data().GetStringAttribute(ax::mojom::StringAttribute::kValue); + // If the value is empty, then there might be some placeholder text in the + // text field, or any other name that is derived from visible contents, even + // if the text field has no children. + if (!value.empty()) + return value; + } + + // Ordinarily, plain text fields are leaves. We need to exclude them from the + // set of leaf nodes when they expose any descendants if we want to compute + // their inner text from their descendant text nodes. + if (IsLeaf() && !(data().IsTextField() && !children().empty())) { + switch (data().GetNameFrom()) { + case ax::mojom::NameFrom::kNone: + case ax::mojom::NameFrom::kUninitialized: + // The accessible name is not displayed on screen, e.g. aria-label, or is + // not displayed directly inside the node, e.g. an associated label + // element. + case ax::mojom::NameFrom::kAttribute: + // The node's accessible name is explicitly empty. + case ax::mojom::NameFrom::kAttributeExplicitlyEmpty: + // The accessible name does not represent the entirety of the node's inner + // text, e.g. a table's caption or a figure's figcaption. + case ax::mojom::NameFrom::kCaption: + case ax::mojom::NameFrom::kRelatedElement: + // The accessible name is not displayed directly inside the node but is + // visible via e.g. a tooltip. + case ax::mojom::NameFrom::kTitle: + return std::string(); + + case ax::mojom::NameFrom::kContents: + // The placeholder text is initially displayed inside the text field and + // takes the place of its value. + case ax::mojom::NameFrom::kPlaceholder: + // The value attribute takes the place of the node's inner text, e.g. the + // value of a submit button is displayed inside the button itself. + case ax::mojom::NameFrom::kValue: + return data().GetStringAttribute(ax::mojom::StringAttribute::kName); + } + } + + std::string inner_text; + for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) { + inner_text += it->GetInnerText(); + } + return inner_text; +} + std::string AXNode::GetLanguage() const { // Walk up tree considering both detected and author declared languages. for (const AXNode* cur = this; cur; cur = cur->parent()) { @@ -584,44 +645,52 @@ AXNode* AXNode::GetTableCellFromCoords(int row_index, int col_index) const { table_info->cell_ids[size_t{row_index}][size_t{col_index}]); } -void AXNode::GetTableColHeaderNodeIds( - int col_index, - std::vector<int32_t>* col_header_ids) const { - DCHECK(col_header_ids); +std::vector<AXNode::AXID> AXNode::GetTableColHeaderNodeIds() const { + const AXTableInfo* table_info = GetAncestorTableInfo(); + if (!table_info) + return std::vector<AXNode::AXID>(); + + std::vector<AXNode::AXID> col_header_ids; + // Flatten and add column header ids of each column to |col_header_ids|. + for (std::vector<AXNode::AXID> col_headers_at_index : + table_info->col_headers) { + col_header_ids.insert(col_header_ids.end(), col_headers_at_index.begin(), + col_headers_at_index.end()); + } + + return col_header_ids; +} + +std::vector<AXNode::AXID> AXNode::GetTableColHeaderNodeIds( + int col_index) const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) - return; + return std::vector<AXNode::AXID>(); if (col_index < 0 || size_t{col_index} >= table_info->col_count) - return; + return std::vector<AXNode::AXID>(); - for (size_t i = 0; i < table_info->col_headers[size_t{col_index}].size(); i++) - col_header_ids->push_back(table_info->col_headers[size_t{col_index}][i]); + return std::vector<AXNode::AXID>(table_info->col_headers[size_t{col_index}]); } -void AXNode::GetTableRowHeaderNodeIds( - int row_index, - std::vector<int32_t>* row_header_ids) const { - DCHECK(row_header_ids); +std::vector<AXNode::AXID> AXNode::GetTableRowHeaderNodeIds( + int row_index) const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) - return; + return std::vector<AXNode::AXID>(); if (row_index < 0 || size_t{row_index} >= table_info->row_count) - return; + return std::vector<AXNode::AXID>(); - for (size_t i = 0; i < table_info->row_headers[size_t{row_index}].size(); i++) - row_header_ids->push_back(table_info->row_headers[size_t{row_index}][i]); + return std::vector<AXNode::AXID>(table_info->row_headers[size_t{row_index}]); } -void AXNode::GetTableUniqueCellIds(std::vector<int32_t>* cell_ids) const { - DCHECK(cell_ids); +std::vector<AXNode::AXID> AXNode::GetTableUniqueCellIds() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info) - return; + return std::vector<AXNode::AXID>(); - cell_ids->assign(table_info->unique_cell_ids.begin(), - table_info->unique_cell_ids.end()); + return std::vector<AXNode::AXID>(table_info->unique_cell_ids); } const std::vector<AXNode*>* AXNode::GetExtraMacNodes() const { @@ -792,47 +861,39 @@ base::Optional<int> AXNode::GetTableCellAriaRowIndex() const { return int{table_info->cell_data_vector[*index].aria_row_index}; } -void AXNode::GetTableCellColHeaderNodeIds( - std::vector<int32_t>* col_header_ids) const { - DCHECK(col_header_ids); +std::vector<AXNode::AXID> AXNode::GetTableCellColHeaderNodeIds() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info || table_info->col_count <= 0) - return; + return std::vector<AXNode::AXID>(); // If this node is not a cell, then return the headers for the first column. int col_index = GetTableCellColIndex().value_or(0); - const auto& col = table_info->col_headers[col_index]; - for (int header : col) - col_header_ids->push_back(header); + + return std::vector<AXNode::AXID>(table_info->col_headers[col_index]); } void AXNode::GetTableCellColHeaders(std::vector<AXNode*>* col_headers) const { DCHECK(col_headers); - std::vector<int32_t> col_header_ids; - GetTableCellColHeaderNodeIds(&col_header_ids); + std::vector<int32_t> col_header_ids = GetTableCellColHeaderNodeIds(); IdVectorToNodeVector(col_header_ids, col_headers); } -void AXNode::GetTableCellRowHeaderNodeIds( - std::vector<int32_t>* row_header_ids) const { - DCHECK(row_header_ids); +std::vector<AXNode::AXID> AXNode::GetTableCellRowHeaderNodeIds() const { const AXTableInfo* table_info = GetAncestorTableInfo(); if (!table_info || table_info->row_count <= 0) - return; + return std::vector<AXNode::AXID>(); // If this node is not a cell, then return the headers for the first row. int row_index = GetTableCellRowIndex().value_or(0); - const auto& row = table_info->row_headers[row_index]; - for (int header : row) - row_header_ids->push_back(header); + + return std::vector<AXNode::AXID>(table_info->row_headers[row_index]); } void AXNode::GetTableCellRowHeaders(std::vector<AXNode*>* row_headers) const { DCHECK(row_headers); - std::vector<int32_t> row_header_ids; - GetTableCellRowHeaderNodeIds(&row_header_ids); + std::vector<int32_t> row_header_ids = GetTableCellRowHeaderNodeIds(); IdVectorToNodeVector(row_header_ids, row_headers); } @@ -902,51 +963,14 @@ bool AXNode::IsOrderedSet() const { return ui::IsSetLike(data().role); } -// pos_in_set and set_size related functions. -// Uses AXTree's cache to calculate node's pos_in_set. +// Uses AXTree's cache to calculate node's PosInSet. base::Optional<int> AXNode::GetPosInSet() { - // Only allow this to be called on nodes that can hold pos_in_set values, - // which are defined in the ARIA spec. - if (!IsOrderedSetItem() || IsIgnored()) - return base::nullopt; - - const AXNode* ordered_set = GetOrderedSet(); - if (!ordered_set) { - return base::nullopt; - } - - // If tree is being updated, return no value. - if (tree()->GetTreeUpdateInProgressState()) - return base::nullopt; - - // See AXTree::GetPosInSet - return tree_->GetPosInSet(*this, ordered_set); + return tree_->GetPosInSet(*this); } -// Uses AXTree's cache to calculate node's set_size. +// Uses AXTree's cache to calculate node's SetSize. base::Optional<int> AXNode::GetSetSize() { - // Only allow this to be called on nodes that can hold set_size values, which - // are defined in the ARIA spec. - if ((!IsOrderedSetItem() && !IsOrderedSet()) || IsIgnored()) - return base::nullopt; - - // If node is item-like, find its outerlying ordered set. Otherwise, - // this node is the ordered set. - const AXNode* ordered_set = this; - if (IsItemLike(data().role)) - ordered_set = GetOrderedSet(); - if (!ordered_set) - return base::nullopt; - - // If tree is being updated, return no value. - if (tree()->GetTreeUpdateInProgressState()) - return base::nullopt; - - // See AXTree::GetSetSize - int32_t set_size = tree_->GetSetSize(*this, ordered_set); - if (set_size < 0) - return base::nullopt; - return set_size; + return tree_->GetSetSize(*this); } // Returns true if the role of ordered set matches the role of item. @@ -1001,7 +1025,8 @@ bool AXNode::SetRoleMatchesItemRole(const AXNode* ordered_set) const { } bool AXNode::IsIgnoredContainerForOrderedSet() const { - return IsIgnored() || data().role == ax::mojom::Role::kListItem || + return IsIgnored() || IsEmbeddedGroup() || + data().role == ax::mojom::Role::kListItem || data().role == ax::mojom::Role::kGenericContainer || data().role == ax::mojom::Role::kUnknown; } @@ -1020,17 +1045,16 @@ int AXNode::UpdateUnignoredCachedValuesRecursive(int startIndex) { return count; } -// Finds ordered set that immediately contains node. +// Finds ordered set that contains node. // Is not required for set's role to match node's role. AXNode* AXNode::GetOrderedSet() const { AXNode* result = parent(); // Continue walking up while parent is invalid, ignored, a generic container, - // or unknown. - while (result && (result->IsIgnored() || - result->data().role == ax::mojom::Role::kGenericContainer || - result->data().role == ax::mojom::Role::kUnknown)) { + // unknown, or embedded group. + while (result && result->IsIgnoredContainerForOrderedSet()) { result = result->parent(); } + return result; } @@ -1069,6 +1093,70 @@ bool AXNode::IsIgnored() const { return data().IsIgnored(); } +bool AXNode::IsChildOfLeaf() const { + const AXNode* ancestor = GetUnignoredParent(); + while (ancestor) { + if (ancestor->IsLeaf()) + return true; + ancestor = ancestor->GetUnignoredParent(); + } + return false; +} + +bool AXNode::IsLeaf() const { + return !GetUnignoredChildCount() || IsLeafIncludingIgnored(); +} + +bool AXNode::IsLeafIncludingIgnored() const { + if (children().empty()) + return true; + +#if defined(OS_WIN) + // On Windows, we want to hide the subtree of a collapsed <select> element. + // Otherwise, ATs are always going to announce its options whether it's + // collapsed or expanded. In the AXTree, this element corresponds to a node + // with role ax::mojom::Role::kPopUpButton that is the parent of a node with + // role ax::mojom::Role::kMenuListPopup. + if (IsCollapsedMenuListPopUpButton()) + return true; +#endif // defined(OS_WIN) + + // These types of objects may have children that we use as internal + // implementation details, but we want to expose them as leaves to platform + // accessibility APIs because screen readers might be confused if they find + // any children. + if (data().IsPlainTextField() || IsText()) + return true; + + // Roles whose children are only presentational according to the ARIA and + // HTML5 Specs should be hidden from screen readers. + switch (data().role) { + // According to the ARIA and Core-AAM specs: + // https://w3c.github.io/aria/#button, + // https://www.w3.org/TR/core-aam-1.1/#exclude_elements + // buttons' children are presentational only and should be hidden from + // screen readers. However, we cannot enforce the leafiness of buttons + // because they may contain many rich, interactive descendants such as a day + // in a calendar, and screen readers will need to interact with these + // contents. See https://crbug.com/689204. + // So we decided to not enforce the leafiness of buttons and expose all + // children. + case ax::mojom::Role::kButton: + return false; + case ax::mojom::Role::kDocCover: + case ax::mojom::Role::kGraphicsSymbol: + case ax::mojom::Role::kImage: + case ax::mojom::Role::kMeter: + case ax::mojom::Role::kScrollBar: + case ax::mojom::Role::kSlider: + case ax::mojom::Role::kSplitter: + case ax::mojom::Role::kProgressIndicator: + return true; + default: + return false; + } +} + bool AXNode::IsInListMarker() const { if (data().role == ax::mojom::Role::kListMarker) return true; @@ -1125,4 +1213,24 @@ AXNode* AXNode::GetCollapsedMenuListPopUpButtonAncestor() const { return node->IsCollapsedMenuListPopUpButton() ? node : nullptr; } +bool AXNode::IsEmbeddedGroup() const { + if (data().role != ax::mojom::Role::kGroup || !parent()) + return false; + + return ui::IsSetLike(parent()->data().role); +} + +AXNode* AXNode::GetTextFieldAncestor() const { + AXNode* parent = GetUnignoredParent(); + + while (parent && parent->data().HasState(ax::mojom::State::kEditable)) { + if (parent->data().IsPlainTextField() || parent->data().IsRichTextField()) + return parent; + + parent = parent->GetUnignoredParent(); + } + + return nullptr; +} + } // namespace ui diff --git a/chromium/ui/accessibility/ax_node.h b/chromium/ui/accessibility/ax_node.h index e19d1bc43b7..b9fb97ff04e 100644 --- a/chromium/ui/accessibility/ax_node.h +++ b/chromium/ui/accessibility/ax_node.h @@ -56,10 +56,9 @@ class AX_EXPORT AXNode final { // See AXTree::GetFromId. virtual AXNode* GetFromId(int32_t id) const = 0; - virtual int32_t GetPosInSet(const AXNode& node, - const AXNode* ordered_set) = 0; - virtual int32_t GetSetSize(const AXNode& node, - const AXNode* ordered_set) = 0; + virtual base::Optional<int> GetPosInSet(const AXNode& node) = 0; + virtual base::Optional<int> GetSetSize(const AXNode& node) = 0; + virtual Selection GetUnignoredSelection() const = 0; virtual bool GetTreeUpdateInProgressState() const = 0; virtual bool HasPaginationSupport() const = 0; @@ -101,7 +100,7 @@ class AX_EXPORT AXNode final { // Accessors. OwnerTree* tree() const { return tree_; } - int32_t id() const { return data_.id; } + AXID id() const { return data_.id; } AXNode* parent() const { return parent_; } const AXNodeData& data() const { return data_; } const std::vector<AXNode*>& children() const { return children_; } @@ -296,6 +295,14 @@ class AX_EXPORT AXNode final { base::string16 GetInheritedString16Attribute( ax::mojom::StringAttribute attribute) const; + // Returns the text of this node and all descendant nodes; including text + // found in embedded objects. + // + // Only text displayed on screen is included. Text from ARIA and HTML + // attributes that is either not displayed on screen, or outside this node, is + // not returned. + std::string GetInnerText() const; + // Return a string representing the language code. // // This will consider the language declared in the DOM, and may eventually @@ -335,11 +342,13 @@ class AX_EXPORT AXNode final { AXNode* GetTableCaption() const; AXNode* GetTableCellFromIndex(int index) const; AXNode* GetTableCellFromCoords(int row_index, int col_index) const; - void GetTableColHeaderNodeIds(int col_index, - std::vector<int32_t>* col_header_ids) const; - void GetTableRowHeaderNodeIds(int row_index, - std::vector<int32_t>* row_header_ids) const; - void GetTableUniqueCellIds(std::vector<int32_t>* row_header_ids) const; + // Get all the column header node ids of the table. + std::vector<AXNode::AXID> GetTableColHeaderNodeIds() const; + // Get the column header node ids associated with |col_index|. + std::vector<AXNode::AXID> GetTableColHeaderNodeIds(int col_index) const; + // Get the row header node ids associated with |row_index|. + std::vector<AXNode::AXID> GetTableRowHeaderNodeIds(int row_index) const; + std::vector<AXNode::AXID> GetTableUniqueCellIds() const; // Extra computed nodes for the accessibility tree for macOS: // one column node for each table column, followed by one // table header container node, or nullptr if not applicable. @@ -366,8 +375,8 @@ class AX_EXPORT AXNode final { base::Optional<int> GetTableCellRowSpan() const; base::Optional<int> GetTableCellAriaColIndex() const; base::Optional<int> GetTableCellAriaRowIndex() const; - void GetTableCellColHeaderNodeIds(std::vector<int32_t>* col_header_ids) const; - void GetTableCellRowHeaderNodeIds(std::vector<int32_t>* row_header_ids) const; + std::vector<AXNode::AXID> GetTableCellColHeaderNodeIds() const; + std::vector<AXNode::AXID> GetTableCellRowHeaderNodeIds() const; void GetTableCellColHeaders(std::vector<AXNode*>* col_headers) const; void GetTableCellRowHeaders(std::vector<AXNode*>* row_headers) const; @@ -390,10 +399,39 @@ class AX_EXPORT AXNode final { // Destroy the language info for this node. void ClearLanguageInfo(); + // Returns true if node is a group and is a direct descendant of a set-like + // element. + bool IsEmbeddedGroup() const; + // Returns true if node has ignored state or ignored role. bool IsIgnored() const; - // Returns true if this current node is a list marker or if it's a descendant + // Returns true if an ancestor of this node (not including itself) is a + // leaf node, meaning that this node is not actually exposed to any + // platform's accessibility layer. + bool IsChildOfLeaf() const; + + // Returns true if this is a leaf node, meaning all its + // children should not be exposed to any platform's native accessibility + // layer. + // + // The definition of a leaf includes nodes with children that are exclusively + // an internal renderer implementation, such as the children of an HTML native + // text field, as well as nodes with presentational children according to the + // ARIA and HTML5 Specs. + // + // A leaf node should never have children that are focusable or + // that might send notifications. + bool IsLeaf() const; + + // Returns true if this is a leaf node, (see "IsLeaf"), or if all of the + // node's children are ignored. + // + // TODO(nektar): There are no performance advantages in keeping this method + // since unignored child count is cached. Please remove. + bool IsLeafIncludingIgnored() const; + + // Returns true if this node is a list marker or if it's a descendant // of a list marker node. Returns false otherwise. bool IsInListMarker() const; @@ -406,6 +444,12 @@ class AX_EXPORT AXNode final { // collapsed. AXNode* GetCollapsedMenuListPopUpButtonAncestor() const; + // Returns the text field ancestor of this current node if any. + AXNode* GetTextFieldAncestor() const; + + // Finds and returns a pointer to ordered set containing node. + AXNode* GetOrderedSet() const; + private: // Computes the text offset where each line starts by traversing all child // leaf nodes. @@ -419,9 +463,6 @@ class AX_EXPORT AXNode final { AXNode* ComputeLastUnignoredChildRecursive() const; AXNode* ComputeFirstUnignoredChildRecursive() const; - // Finds and returns a pointer to ordered set containing node. - AXNode* GetOrderedSet() const; - OwnerTree* const tree_; // Owns this. size_t index_in_parent_; size_t unignored_index_in_parent_; @@ -430,6 +471,7 @@ class AX_EXPORT AXNode final { std::vector<AXNode*> children_; AXNodeData data_; + // Stores the detected language computed from the node's text. std::unique_ptr<AXLanguageInfo> language_info_; }; diff --git a/chromium/ui/accessibility/ax_node_data.cc b/chromium/ui/accessibility/ax_node_data.cc index 1780711a1ae..789e9e01feb 100644 --- a/chromium/ui/accessibility/ax_node_data.cc +++ b/chromium/ui/accessibility/ax_node_data.cc @@ -570,17 +570,42 @@ AXNodeTextStyles AXNodeData::GetTextStyles() const { } void AXNodeData::SetName(const std::string& name) { + DCHECK_NE(role, ax::mojom::Role::kNone) + << "A valid role is required before setting the name attribute, because " + "the role is used for setting the required NameFrom attribute."; + auto iter = std::find_if(string_attributes.begin(), string_attributes.end(), [](const auto& string_attribute) { return string_attribute.first == ax::mojom::StringAttribute::kName; }); + if (iter == string_attributes.end()) { string_attributes.push_back( std::make_pair(ax::mojom::StringAttribute::kName, name)); } else { iter->second = name; } + + if (HasIntAttribute(ax::mojom::IntAttribute::kNameFrom)) + return; + // Since this method is mostly used by tests which don't always set the + // "NameFrom" attribute, we need to set it here to the most likely value if + // not set, otherwise code that tries to calculate the node's inner text, its + // hypertext, or even its value, might not know whether to include the name in + // the result or not. + // + // For example, if there is a text field, but it is empty, i.e. it has no + // value, its value could be its name if "NameFrom" is set to "kPlaceholder" + // or to "kContents" but not if it's set to "kAttribute". Similarly, if there + // is a button without any unignored children, it's name can only be + // equivalent to its inner text if "NameFrom" is set to "kContents" or to + // "kValue", but not if it is set to "kAttribute". + if (IsText(role)) { + SetNameFrom(ax::mojom::NameFrom::kContents); + } else { + SetNameFrom(ax::mojom::NameFrom::kAttribute); + } } void AXNodeData::SetName(const base::string16& name) { @@ -729,7 +754,7 @@ ax::mojom::CheckedState AXNodeData::GetCheckedState() const { } void AXNodeData::SetCheckedState(ax::mojom::CheckedState checked_state) { - if (HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) + if (HasCheckedState()) RemoveIntAttribute(ax::mojom::IntAttribute::kCheckedState); if (checked_state != ax::mojom::CheckedState::kNone) { AddIntAttribute(ax::mojom::IntAttribute::kCheckedState, @@ -737,6 +762,10 @@ void AXNodeData::SetCheckedState(ax::mojom::CheckedState checked_state) { } } +bool AXNodeData::HasCheckedState() const { + return HasIntAttribute(ax::mojom::IntAttribute::kCheckedState); +} + ax::mojom::DefaultActionVerb AXNodeData::GetDefaultActionVerb() const { return static_cast<ax::mojom::DefaultActionVerb>( GetIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb)); @@ -1001,21 +1030,6 @@ bool AXNodeData::SupportsExpandCollapse() const { return ui::SupportsExpandCollapse(role); } -bool AXNodeData::IsContainedInActiveLiveRegion() const { - if (!HasStringAttribute(ax::mojom::StringAttribute::kContainerLiveStatus)) - return false; - - if (base::CompareCaseInsensitiveASCII( - GetStringAttribute(ax::mojom::StringAttribute::kContainerLiveStatus), - "off") == 0) - return false; - - if (GetBoolAttribute(ax::mojom::BoolAttribute::kContainerLiveBusy)) - return false; - - return true; -} - std::string AXNodeData::ToString() const { std::string result; @@ -1539,6 +1553,9 @@ std::string AXNodeData::ToString() const { case ax::mojom::BoolAttribute::kSelected: result += " selected=" + value; break; + case ax::mojom::BoolAttribute::kSelectedFromFocus: + result += " selected_from_focus=" + value; + break; case ax::mojom::BoolAttribute::kSupportsTextLocation: result += " supports_text_location=" + value; break; diff --git a/chromium/ui/accessibility/ax_node_data.h b/chromium/ui/accessibility/ax_node_data.h index e03e836b912..c2ac3b5ad75 100644 --- a/chromium/ui/accessibility/ax_node_data.h +++ b/chromium/ui/accessibility/ax_node_data.h @@ -141,7 +141,8 @@ struct AX_BASE_EXPORT AXNodeData { // Convenience functions. // - // Adds the name attribute or replaces it if already present. + // Adds the name attribute or replaces it if already present. Also sets the + // NameFrom attribute if not already set. void SetName(const std::string& name); void SetName(const base::string16& name); @@ -177,6 +178,7 @@ struct AX_BASE_EXPORT AXNodeData { // Please keep in alphabetic order. ax::mojom::CheckedState GetCheckedState() const; void SetCheckedState(ax::mojom::CheckedState checked_state); + bool HasCheckedState() const; ax::mojom::DefaultActionVerb GetDefaultActionVerb() const; void SetDefaultActionVerb(ax::mojom::DefaultActionVerb default_action_verb); ax::mojom::HasPopup GetHasPopup() const; @@ -258,9 +260,6 @@ struct AX_BASE_EXPORT AXNodeData { // expand/collapse. bool SupportsExpandCollapse() const; - // Helper to determine if the node is in an active live region. - bool IsContainedInActiveLiveRegion() const; - // Return a string representation of this data, for debugging. virtual std::string ToString() const; diff --git a/chromium/ui/accessibility/ax_node_position.cc b/chromium/ui/accessibility/ax_node_position.cc index ffce6af1700..ef23c509c7e 100644 --- a/chromium/ui/accessibility/ax_node_position.cc +++ b/chromium/ui/accessibility/ax_node_position.cc @@ -18,7 +18,7 @@ AXEmbeddedObjectBehavior g_ax_embedded_object_behavior = AXEmbeddedObjectBehavior::kExposeCharacter; #else AXEmbeddedObjectBehavior::kSuppressCharacter; -#endif +#endif // defined(OS_WIN) // static AXNodePosition::AXPositionInstance AXNodePosition::CreatePosition( @@ -29,9 +29,10 @@ AXNodePosition::AXPositionInstance AXNodePosition::CreatePosition( return CreateNullPosition(); AXTreeID tree_id = node.tree()->GetAXTreeID(); - if (node.IsText()) + if (node.IsText()) { return CreateTextPosition(tree_id, node.id(), child_index_or_text_offset, affinity); + } return CreateTreePosition(tree_id, node.id(), child_index_or_text_offset); } diff --git a/chromium/ui/accessibility/ax_node_position_unittest.cc b/chromium/ui/accessibility/ax_node_position_unittest.cc index d7e7b25695e..170054bbb71 100644 --- a/chromium/ui/accessibility/ax_node_position_unittest.cc +++ b/chromium/ui/accessibility/ax_node_position_unittest.cc @@ -1370,6 +1370,135 @@ TEST_F(AXPositionTest, AtEndOfBlankLine) { EXPECT_TRUE(text_position->AtEndOfLine()); } +TEST_F(AXPositionTest, AtStartAndEndOfLineWhenAtEndOfTextSpan) { + // This test ensures that the "AtStartOfLine" and the "AtEndOfLine" methods + // return false and true respectively when we are at the end of a text span. + // + // A text span is defined by a series of inline text boxes that make up a + // single static text object. Lines always end at the end of static text + // objects, so there would never arise a situation when a position at the end + // of a text span would be at start of line. It should always be at end of + // line. On the contrary, if a position is at the end of an inline text box + // and the equivalent parent position is in the middle of a static text + // object, then the position would sometimes be at start of line, i.e., when + // the inline text box contains only white space that is used to separate + // lines in the case of lines being wrapped by a soft line break. + // + // Example accessibility tree: + // 0:kRootWebArea + // ++1:kStaticText "Hello testing " + // ++++2:kInlineTextBox "Hello" kNextOnLine=2 + // ++++3:kInlineTextBox " " kPreviousOnLine=2 + // ++++4:kInlineTextBox "testing" kNextOnLine=5 + // ++++5:kInlineTextBox " " kPreviousOnLine=4 + // ++6:kStaticText "here." + // ++++7:kInlineTextBox "here." + // + // Resulting text representation: + // "Hello<soft_line_break>testing <hard_line_break>here." + // Notice the extra space after the word "testing". This is not a line break. + // The hard line break is caused by the presence of the second static text + // object. + // + // A position at the end of inline text box 3 should be at start of line, + // whilst a position at the end of inline text box 5 should not. + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + // "kIsLineBreakingObject" is not strictly necessary but is added for + // completeness. + root_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, + true); + + AXNodeData static_text_data_1; + static_text_data_1.id = 2; + static_text_data_1.role = ax::mojom::Role::kStaticText; + static_text_data_1.SetName("Hello testing "); + + AXNodeData inline_box_data_1; + inline_box_data_1.id = 3; + inline_box_data_1.role = ax::mojom::Role::kInlineTextBox; + inline_box_data_1.SetName("hello"); + + AXNodeData inline_box_data_2; + inline_box_data_2.id = 4; + inline_box_data_2.role = ax::mojom::Role::kInlineTextBox; + inline_box_data_1.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, + inline_box_data_2.id); + inline_box_data_2.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, + inline_box_data_1.id); + // The name is a space character that we assume it turns into a soft line + // break by the layout engine. + inline_box_data_2.SetName(" "); + + AXNodeData inline_box_data_3; + inline_box_data_3.id = 5; + inline_box_data_3.role = ax::mojom::Role::kInlineTextBox; + inline_box_data_3.SetName("testing"); + + AXNodeData inline_box_data_4; + inline_box_data_4.id = 6; + inline_box_data_4.role = ax::mojom::Role::kInlineTextBox; + inline_box_data_3.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, + inline_box_data_4.id); + inline_box_data_4.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, + inline_box_data_3.id); + inline_box_data_4.SetName(" "); // Just a space character - not a line break. + + AXNodeData static_text_data_2; + static_text_data_2.id = 7; + static_text_data_2.role = ax::mojom::Role::kStaticText; + static_text_data_2.SetName("here."); + + AXNodeData inline_box_data_5; + inline_box_data_5.id = 8; + inline_box_data_5.role = ax::mojom::Role::kInlineTextBox; + inline_box_data_5.SetName("here."); + + static_text_data_1.child_ids = {inline_box_data_1.id, inline_box_data_2.id, + inline_box_data_3.id, inline_box_data_4.id}; + static_text_data_2.child_ids = {inline_box_data_5.id}; + root_data.child_ids = {static_text_data_1.id, static_text_data_2.id}; + + SetTree(CreateAXTree({root_data, static_text_data_1, inline_box_data_1, + inline_box_data_2, inline_box_data_3, inline_box_data_4, + static_text_data_2, inline_box_data_5})); + + // An "after text" tree position - after the soft line break. + TestPositionType tree_position = AXNodePosition::CreateTreePosition( + GetTreeID(), inline_box_data_2.id, 0 /* child_index */); + ASSERT_NE(nullptr, tree_position); + ASSERT_TRUE(tree_position->IsTreePosition()); + EXPECT_TRUE(tree_position->AtStartOfLine()); + EXPECT_FALSE(tree_position->AtEndOfLine()); + + // An "after text" tree position - after the space character and before the + // hard line break caused by the second static text object. + tree_position = AXNodePosition::CreateTreePosition( + GetTreeID(), inline_box_data_4.id, 0 /* child_index */); + ASSERT_NE(nullptr, tree_position); + ASSERT_TRUE(tree_position->IsTreePosition()); + EXPECT_FALSE(tree_position->AtStartOfLine()); + EXPECT_TRUE(tree_position->AtEndOfLine()); + + TestPositionType text_position = AXNodePosition::CreateTextPosition( + GetTreeID(), inline_box_data_2.id, 1 /* text_offset */, + ax::mojom::TextAffinity::kDownstream); + ASSERT_NE(nullptr, text_position); + ASSERT_TRUE(text_position->IsTextPosition()); + EXPECT_TRUE(text_position->AtStartOfLine()); + EXPECT_FALSE(text_position->AtEndOfLine()); + + text_position = AXNodePosition::CreateTextPosition( + GetTreeID(), inline_box_data_4.id, 1 /* text_offset */, + ax::mojom::TextAffinity::kDownstream); + ASSERT_NE(nullptr, text_position); + ASSERT_TRUE(text_position->IsTextPosition()); + EXPECT_FALSE(text_position->AtStartOfLine()); + EXPECT_TRUE(text_position->AtEndOfLine()); +} + TEST_F(AXPositionTest, AtStartAndEndOfLineInsideTextField) { // This test ensures that "AtStart/EndOfLine" methods work properly when at // the start or end of a text field. diff --git a/chromium/ui/accessibility/ax_param_traits_macros.h b/chromium/ui/accessibility/ax_param_traits_macros.h index d33912c774e..b545df34ba2 100644 --- a/chromium/ui/accessibility/ax_param_traits_macros.h +++ b/chromium/ui/accessibility/ax_param_traits_macros.h @@ -106,6 +106,7 @@ IPC_STRUCT_TRAITS_BEGIN(ui::AXTreeUpdate) IPC_STRUCT_TRAITS_MEMBER(root_id) IPC_STRUCT_TRAITS_MEMBER(nodes) IPC_STRUCT_TRAITS_MEMBER(event_from) + IPC_STRUCT_TRAITS_MEMBER(event_intents) IPC_STRUCT_TRAITS_END() #undef IPC_MESSAGE_EXPORT diff --git a/chromium/ui/accessibility/ax_position.h b/chromium/ui/accessibility/ax_position.h index 70f9501c195..dfeed6edfde 100644 --- a/chromium/ui/accessibility/ax_position.h +++ b/chromium/ui/accessibility/ax_position.h @@ -23,7 +23,6 @@ #include "base/strings/string16.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" -#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node.h" @@ -518,9 +517,12 @@ class AXPosition { // We assume that white space, including but not limited to hard line // breaks, might be used to separate lines. For example, an inline text // box with just a single space character inside it can be used to - // represent a soft line break. if an inline text box containing white + // represent a soft line break. If an inline text box containing white // space separates two lines, it should always be connected to the first - // line via "kPreviousOnLineId". This is guaranteed by the renderer. + // line via "kPreviousOnLineId". This is guaranteed by the renderer. If + // there are multiple line breaks separating the two lines, then only + // the first line break is connected to the first line via + // "kPreviousOnLineId". // // Sometimes there might be an inline text box with a single space in it // at the end of a text field. We should not mark positions that are at @@ -530,11 +532,11 @@ class AXPosition { // all cases, the parent of an inline text box is a static text object, // whose end signifies the end of the text span. One exception is line // breaks. - if (!text_position->AtEndOfTextSpan() && + if (text_position->AtEndOfAnchor() && + !text_position->AtEndOfTextSpan() && text_position->IsInWhiteSpace() && GetNextOnLineID(text_position->anchor_id_) == - AXNode::kInvalidAXID && - text_position->AtEndOfAnchor()) { + AXNode::kInvalidAXID) { return true; } @@ -573,10 +575,12 @@ class AXPosition { // In other cases, we assume that white space, including but not limited // to hard line breaks, might be used to separate lines. For example, an // inline text box with just a single space character inside it can be - // used to represent a soft line break. if an inline text box containing + // used to represent a soft line break. If an inline text box containing // white space separates two lines, it should always be connected to the // first line via "kPreviousOnLineId". This is guaranteed by the - // renderer. + // renderer. If there are multiple line breaks separating the two lines, + // then only the first line break is connected to the first line via + // "kPreviousOnLineId". // // We don't treat a position that is at the start of white space that is // on a line by itself as being at the end of the line. This is in order @@ -641,9 +645,10 @@ class AXPosition { // 2. The current position is not whitespace only, unless it is also // the first leaf text position within the document. - if (text_position->IsInWhiteSpace()) + if (text_position->IsInWhiteSpace()) { return text_position->CreatePreviousLeafTextPosition() ->IsNullPosition(); + } // 3. Either (a) the current leaf text position is the first leaf text // position in the document, or (b) there are no line breaking @@ -3036,7 +3041,7 @@ class AXPosition { if (AnchorUnignoredChildCount()) return false; - // All unignored leaf nodes in the AXTree except the document and the text + // All unignored leaf nodes in the AXTree except document and text // nodes should be replaced by the embedded object character. Also, nodes // that only have ignored children (e.g., a button that contains only an // empty div) need to be treated as leaf nodes. @@ -3083,14 +3088,12 @@ class AXPosition { // The first unignored ancestor is necessarily the empty object if this node // is the descendant of an empty object. AXNodeType* ancestor_node = GetLowestUnignoredAncestor(); - if (!ancestor_node) return nullptr; AXPositionInstance position = CreateTextPosition( tree_id_, GetAnchorID(ancestor_node), 0 /* text_offset */, ax::mojom::TextAffinity::kDownstream); - if (position && position->IsEmptyObjectReplacedByCharacter()) return ancestor_node; diff --git a/chromium/ui/accessibility/ax_range_unittest.cc b/chromium/ui/accessibility/ax_range_unittest.cc index 92910f94a57..b27df725cb6 100644 --- a/chromium/ui/accessibility/ax_range_unittest.cc +++ b/chromium/ui/accessibility/ax_range_unittest.cc @@ -17,7 +17,7 @@ #include "ui/accessibility/ax_tree.h" #include "ui/accessibility/ax_tree_id.h" #include "ui/accessibility/ax_tree_update.h" -#include "ui/accessibility/platform/test_ax_node_wrapper.h" +#include "ui/accessibility/test_ax_node_helper.h" #include "ui/accessibility/test_ax_tree_manager.h" namespace ui { @@ -68,8 +68,8 @@ class TestAXRangeScreenRectDelegate : public AXRangeRectDelegate { if (!node) return gfx::Rect(); - TestAXNodeWrapper* wrapper = - TestAXNodeWrapper::GetOrCreate(tree_manager_->GetTree(), node); + TestAXNodeHelper* wrapper = + TestAXNodeHelper::GetOrCreate(tree_manager_->GetTree(), node); return wrapper->GetInnerTextRangeBoundsRect( start_offset, end_offset, AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kClipped, offscreen_result); @@ -85,8 +85,8 @@ class TestAXRangeScreenRectDelegate : public AXRangeRectDelegate { if (!node) return gfx::Rect(); - TestAXNodeWrapper* wrapper = - TestAXNodeWrapper::GetOrCreate(tree_manager_->GetTree(), node); + TestAXNodeHelper* wrapper = + TestAXNodeHelper::GetOrCreate(tree_manager_->GetTree(), node); return wrapper->GetBoundsRect(AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kClipped, offscreen_result); diff --git a/chromium/ui/accessibility/ax_role_properties.cc b/chromium/ui/accessibility/ax_role_properties.cc index 587b4e81e19..60115f31327 100644 --- a/chromium/ui/accessibility/ax_role_properties.cc +++ b/chromium/ui/accessibility/ax_role_properties.cc @@ -564,10 +564,10 @@ bool IsSetLike(const ax::mojom::Role role) { case ax::mojom::Role::kMenu: case ax::mojom::Role::kMenuBar: case ax::mojom::Role::kMenuListPopup: + case ax::mojom::Role::kPopUpButton: case ax::mojom::Role::kRadioGroup: case ax::mojom::Role::kTabList: case ax::mojom::Role::kTree: - case ax::mojom::Role::kPopUpButton: return true; default: return false; @@ -675,16 +675,6 @@ bool IsTableRow(ax::mojom::Role role) { } } -bool IsTextOrLineBreak(ax::mojom::Role role) { - switch (role) { - case ax::mojom::Role::kLineBreak: - case ax::mojom::Role::kStaticText: - return true; - default: - return false; - } -} - bool IsText(ax::mojom::Role role) { switch (role) { case ax::mojom::Role::kInlineTextBox: diff --git a/chromium/ui/accessibility/ax_role_properties.h b/chromium/ui/accessibility/ax_role_properties.h index 536d2758440..60e8e570e62 100644 --- a/chromium/ui/accessibility/ax_role_properties.h +++ b/chromium/ui/accessibility/ax_role_properties.h @@ -161,14 +161,10 @@ AX_BASE_EXPORT bool IsTableLike(const ax::mojom::Role role); // table is not used for layout purposes. AX_BASE_EXPORT bool IsTableRow(ax::mojom::Role role); -// Returns true if it's a text-related node e.g. static text, line break, or -// inline text box node. +// Returns true if the provided role is text-related, e.g., static text, line +// break, or inline text box. AX_BASE_EXPORT bool IsText(ax::mojom::Role role); -// Returns true if it's a text-related node e.g. a static text or line break -// node. -AX_BASE_EXPORT bool IsTextOrLineBreak(ax::mojom::Role role); - // Returns true if the role supports expand/collapse. AX_BASE_EXPORT bool SupportsExpandCollapse(const ax::mojom::Role role); diff --git a/chromium/ui/accessibility/ax_table_fuzzer.cc b/chromium/ui/accessibility/ax_table_fuzzer.cc index 9925f9bf79f..1ee955ee38f 100644 --- a/chromium/ui/accessibility/ax_table_fuzzer.cc +++ b/chromium/ui/accessibility/ax_table_fuzzer.cc @@ -98,12 +98,21 @@ void TestTableAPIs(const ui::AXNode* node) { // crash. Normally |ids| is an out argument only, but // there's no reason we shouldn't be able to pass a vector // that was previously used by another call. - std::vector<int32_t> ids; + std::vector<ui::AXNode::AXID> ids; for (int i = 0; i < 3; i++) { - node->GetTableColHeaderNodeIds(i, &ids); - node->GetTableRowHeaderNodeIds(i, &ids); + std::vector<ui::AXNode::AXID> col_header_node_ids = + node->GetTableColHeaderNodeIds(i); + ids.insert(ids.end(), col_header_node_ids.begin(), + col_header_node_ids.end()); + + std::vector<ui::AXNode::AXID> row_header_node_ids = + node->GetTableRowHeaderNodeIds(i); + ids.insert(ids.end(), row_header_node_ids.begin(), + row_header_node_ids.end()); } - node->GetTableUniqueCellIds(&ids); + std::vector<ui::AXNode::AXID> unique_cell_ids = node->GetTableUniqueCellIds(); + ids.insert(ids.end(), unique_cell_ids.begin(), unique_cell_ids.end()); + ignore_result(node->IsTableRow()); ignore_result(node->GetTableRowRowIndex()); #if defined(OS_MACOSX) @@ -118,8 +127,14 @@ void TestTableAPIs(const ui::AXNode* node) { ignore_result(node->GetTableCellRowSpan()); ignore_result(node->GetTableCellAriaColIndex()); ignore_result(node->GetTableCellAriaRowIndex()); - node->GetTableCellColHeaderNodeIds(&ids); - node->GetTableCellRowHeaderNodeIds(&ids); + std::vector<ui::AXNode::AXID> cell_col_header_node_ids = + node->GetTableCellColHeaderNodeIds(); + ids.insert(ids.end(), cell_col_header_node_ids.begin(), + cell_col_header_node_ids.end()); + std::vector<ui::AXNode::AXID> cell_row_header_node_ids = + node->GetTableCellRowHeaderNodeIds(); + ids.insert(ids.end(), cell_row_header_node_ids.begin(), + cell_row_header_node_ids.end()); std::vector<ui::AXNode*> headers; node->GetTableCellColHeaders(&headers); node->GetTableCellRowHeaders(&headers); diff --git a/chromium/ui/accessibility/ax_tree.cc b/chromium/ui/accessibility/ax_tree.cc index 5a2b447abbc..003f2770723 100644 --- a/chromium/ui/accessibility/ax_tree.cc +++ b/chromium/ui/accessibility/ax_tree.cc @@ -925,6 +925,11 @@ const std::set<AXTreeID> AXTree::GetAllChildTreeIds() const { } bool AXTree::Unserialize(const AXTreeUpdate& update) { + event_intents_ = update.event_intents; + base::ScopedClosureRunner clear_event_intents(base::BindOnce( + [](std::vector<AXEventIntent>* event_intents) { event_intents->clear(); }, + &event_intents_)); + AXTreeUpdateState update_state(*this); const AXNode::AXID old_root_id = root_ ? root_->id() : AXNode::kInvalidAXID; @@ -2202,30 +2207,69 @@ void AXTree::ComputeSetSizePosInSetAndCacheHelper( } // End of iterating over each item in |ordered_set_content|. } -// Returns the pos_in_set of item. Looks in |node_set_size_pos_in_set_info_map_| -// for cached value. Calculates pos_in_set and set_size for item (and all other -// items in the same ordered set) if no value is present in the cache. This -// function is guaranteed to be only called on nodes that can hold pos_in_set -// values, minimizing the size of the cache. -int32_t AXTree::GetPosInSet(const AXNode& node, const AXNode* ordered_set) { - // If item's id is not in the cache, compute it. - if (node_set_size_pos_in_set_info_map_.find(node.id()) == - node_set_size_pos_in_set_info_map_.end()) - ComputeSetSizePosInSetAndCache(node, ordered_set); - return node_set_size_pos_in_set_info_map_[node.id()].pos_in_set; +base::Optional<int> AXTree::GetPosInSet(const AXNode& node) { + if (node_set_size_pos_in_set_info_map_.find(node.id()) != + node_set_size_pos_in_set_info_map_.end()) { + // If item's id is in the cache, return stored PosInSet value. + return node_set_size_pos_in_set_info_map_[node.id()].pos_in_set; + } + + if (GetTreeUpdateInProgressState()) + return base::nullopt; + + // Only allow this to be called on nodes that can hold PosInSet values, + // which are defined in the ARIA spec. + if (!node.IsOrderedSetItem() || node.IsIgnored()) + return base::nullopt; + + const AXNode* ordered_set = node.GetOrderedSet(); + if (!ordered_set) + return base::nullopt; + + // Compute, cache, then return. + ComputeSetSizePosInSetAndCache(node, ordered_set); + base::Optional<int> pos_in_set = + node_set_size_pos_in_set_info_map_[node.id()].pos_in_set; + if (pos_in_set.has_value() && pos_in_set.value() < 1) + return base::nullopt; + + return pos_in_set; } -// Returns the set_size of node. node could be an ordered set or an item. -// Looks in |node_set_size_pos_in_set_info_map_| for cached value. Calculates -// pos_in_set and set_size for all nodes in same ordered set if no value is -// present in the cache. This function is guaranteed to be only called on nodes -// that can hold set_size values, minimizing the size of the cache. -int32_t AXTree::GetSetSize(const AXNode& node, const AXNode* ordered_set) { - // If node's id is not in the cache, compute it. - if (node_set_size_pos_in_set_info_map_.find(node.id()) == - node_set_size_pos_in_set_info_map_.end()) - ComputeSetSizePosInSetAndCache(node, ordered_set); - return node_set_size_pos_in_set_info_map_[node.id()].set_size; +base::Optional<int> AXTree::GetSetSize(const AXNode& node) { + if (node_set_size_pos_in_set_info_map_.find(node.id()) != + node_set_size_pos_in_set_info_map_.end()) { + // If item's id is in the cache, return stored SetSize value. + return node_set_size_pos_in_set_info_map_[node.id()].set_size; + } + + if (GetTreeUpdateInProgressState()) + return base::nullopt; + + // Only allow this to be called on nodes that can hold SetSize values, which + // are defined in the ARIA spec. However, we allow set-like items to receive + // SetSize values for internal purposes. + if ((!node.IsOrderedSetItem() && !node.IsOrderedSet()) || node.IsIgnored() || + node.IsEmbeddedGroup()) { + return base::nullopt; + } + + // If |node| is item-like, find its outerlying ordered set. Otherwise, + // |node| is the ordered set. + const AXNode* ordered_set = &node; + if (IsItemLike(node.data().role)) + ordered_set = node.GetOrderedSet(); + if (!ordered_set) + return base::nullopt; + + // Compute, cache, then return. + ComputeSetSizePosInSetAndCache(node, ordered_set); + base::Optional<int> set_size = + node_set_size_pos_in_set_info_map_[node.id()].set_size; + if (set_size.has_value() && set_size.value() < 0) + return base::nullopt; + + return set_size; } AXTree::Selection AXTree::GetUnignoredSelection() const { diff --git a/chromium/ui/accessibility/ax_tree.h b/chromium/ui/accessibility/ax_tree.h index 8c1c57517ac..c47d1c20234 100644 --- a/chromium/ui/accessibility/ax_tree.h +++ b/chromium/ui/accessibility/ax_tree.h @@ -147,18 +147,14 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { // conflict with positive-numbered node IDs from tree sources. int32_t GetNextNegativeInternalNodeId(); - // Returns the pos_in_set of node. Looks in node_set_size_pos_in_set_info_map_ - // for cached value. Calculates pos_in_set and set_size for node (and all - // other nodes in the same ordered set) if no value is present in the cache. - // This function is guaranteed to be only called on nodes that can hold - // pos_in_set values, minimizing the size of the cache. - int32_t GetPosInSet(const AXNode& node, const AXNode* ordered_set) override; - // Returns the set_size of node. Looks in node_set_size_pos_in_set_info_map_ - // for cached value. Calculates pos_inset_set and set_size for node (and all - // other nodes in the same ordered set) if no value is present in the cache. - // This function is guaranteed to be only called on nodes that can hold - // set_size values, minimizing the size of the cache. - int32_t GetSetSize(const AXNode& node, const AXNode* ordered_set) override; + // Returns the PosInSet of |node|. Looks in node_set_size_pos_in_set_info_map_ + // for cached value. Calls |ComputeSetSizePosInSetAndCache|if no value is + // present in the cache. + base::Optional<int> GetPosInSet(const AXNode& node) override; + // Returns the SetSize of |node|. Looks in node_set_size_pos_in_set_info_map_ + // for cached value. Calls |ComputeSetSizePosInSetAndCache|if no value is + // present in the cache. + base::Optional<int> GetSetSize(const AXNode& node) override; Selection GetUnignoredSelection() const override; @@ -174,6 +170,11 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { // When should we initialize this? std::unique_ptr<AXLanguageDetectionManager> language_detection_manager; + // A list of intents active during a tree update/unserialization. + const std::vector<AXEventIntent>& event_intents() const { + return event_intents_; + } + private: friend class AXTableInfoTest; @@ -332,8 +333,8 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { NodeSetSizePosInSetInfo(); ~NodeSetSizePosInSetInfo(); - int32_t pos_in_set = 0; - int32_t set_size = 0; + base::Optional<int> pos_in_set; + base::Optional<int> set_size; base::Optional<int> lowest_hierarchical_level; }; @@ -388,6 +389,8 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { // Indicates if the tree represents a paginated document bool has_pagination_support_ = false; + + std::vector<AXEventIntent> event_intents_; }; } // namespace ui diff --git a/chromium/ui/accessibility/ax_tree_id.cc b/chromium/ui/accessibility/ax_tree_id.cc index 86347de4255..3cb2335c798 100644 --- a/chromium/ui/accessibility/ax_tree_id.cc +++ b/chromium/ui/accessibility/ax_tree_id.cc @@ -10,7 +10,7 @@ #include "base/check.h" #include "base/no_destructor.h" #include "base/notreached.h" -#include "base/value_conversions.h" +#include "base/util/values/values_util.h" #include "base/values.h" #include "ui/accessibility/ax_enums.mojom.h" @@ -30,10 +30,10 @@ AXTreeID::AXTreeID(const std::string& string) { type_ = ax::mojom::AXTreeIDType::kUnknown; } else { type_ = ax::mojom::AXTreeIDType::kToken; - base::Value string_value(string); - base::UnguessableToken token; - CHECK(base::GetValueAsUnguessableToken(string_value, &token)); - token_ = token; + base::Optional<base::UnguessableToken> token = + util::ValueToUnguessableToken(base::Value(string)); + CHECK(token); + token_ = *token; } } @@ -54,7 +54,7 @@ std::string AXTreeID::ToString() const { case ax::mojom::AXTreeIDType::kUnknown: return ""; case ax::mojom::AXTreeIDType::kToken: - return base::CreateUnguessableTokenValue(*token_).GetString(); + return util::UnguessableTokenToValue(*token_).GetString(); } NOTREACHED(); diff --git a/chromium/ui/accessibility/ax_tree_unittest.cc b/chromium/ui/accessibility/ax_tree_unittest.cc index 05c962720ea..8f2c3ce8541 100644 --- a/chromium/ui/accessibility/ax_tree_unittest.cc +++ b/chromium/ui/accessibility/ax_tree_unittest.cc @@ -3138,7 +3138,7 @@ TEST(AXTreeTest, ChildTreeIds) { } // Tests GetPosInSet and GetSetSize return the assigned int attribute values. -TEST(AXTreeTest, TestSetSizePosInSetAssigned) { +TEST(AXTreeTest, SetSizePosInSetAssigned) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); @@ -3170,8 +3170,8 @@ TEST(AXTreeTest, TestSetSizePosInSetAssigned) { EXPECT_OPTIONAL_EQ(12, item3->GetSetSize()); } -// Tests that pos_in_set and set_size can be calculated if not assigned. -TEST(AXTreeTest, TestSetSizePosInSetUnassigned) { +// Tests that PosInSet and SetSize can be calculated if not assigned. +TEST(AXTreeTest, SetSizePosInSetUnassigned) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); @@ -3197,9 +3197,9 @@ TEST(AXTreeTest, TestSetSizePosInSetUnassigned) { EXPECT_OPTIONAL_EQ(3, item3->GetSetSize()); } -// Tests pos_in_set can be calculated if unassigned, and set_size can be +// Tests PosInSet can be calculated if unassigned, and SetSize can be // assigned on the outerlying ordered set. -TEST(AXTreeTest, TestSetSizeAssignedInContainer) { +TEST(AXTreeTest, SetSizeAssignedOnContainer) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); @@ -3215,18 +3215,21 @@ TEST(AXTreeTest, TestSetSizeAssignedInContainer) { tree_update.nodes[3].role = ax::mojom::Role::kListItem; AXTree tree(tree_update); - // Items should inherit set_size from ordered set if not specified. + // Items should inherit SetSize from ordered set if not specified. AXNode* item1 = tree.GetFromId(2); EXPECT_OPTIONAL_EQ(7, item1->GetSetSize()); + EXPECT_OPTIONAL_EQ(1, item1->GetPosInSet()); AXNode* item2 = tree.GetFromId(3); EXPECT_OPTIONAL_EQ(7, item2->GetSetSize()); + EXPECT_OPTIONAL_EQ(2, item2->GetPosInSet()); AXNode* item3 = tree.GetFromId(4); EXPECT_OPTIONAL_EQ(7, item3->GetSetSize()); + EXPECT_OPTIONAL_EQ(3, item3->GetPosInSet()); } // Tests GetPosInSet and GetSetSize on a list containing various roles. // Roles for items and associated ordered set should match up. -TEST(AXTreeTest, TestSetSizePosInSetDiverseList) { +TEST(AXTreeTest, SetSizePosInSetDiverseList) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(6); @@ -3261,12 +3264,12 @@ TEST(AXTreeTest, TestSetSizePosInSetDiverseList) { EXPECT_OPTIONAL_EQ(4, item3->GetPosInSet()); EXPECT_OPTIONAL_EQ(4, item3->GetSetSize()); AXNode* tab = tree.GetFromId(6); - EXPECT_OPTIONAL_EQ(0, tab->GetPosInSet()); - EXPECT_OPTIONAL_EQ(0, tab->GetSetSize()); + EXPECT_FALSE(tab->GetPosInSet()); + EXPECT_FALSE(tab->GetSetSize()); } // Tests GetPosInSet and GetSetSize on a nested list. -TEST(AXTreeTest, TestSetSizePosInSetNestedList) { +TEST(AXTreeTest, SetSizePosInSetNestedList) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(7); @@ -3307,9 +3310,9 @@ TEST(AXTreeTest, TestSetSizePosInSetNestedList) { EXPECT_OPTIONAL_EQ(3, outer_item3->GetSetSize()); } -// Tests pos_in_set can be calculated if one item specifies pos_in_set, but +// Tests PosInSet can be calculated if one item specifies PosInSet, but // other assignments are missing. -TEST(AXTreeTest, TestPosInSetMissing) { +TEST(AXTreeTest, PosInSetMissing) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); @@ -3339,8 +3342,8 @@ TEST(AXTreeTest, TestPosInSetMissing) { EXPECT_OPTIONAL_EQ(20, item3->GetSetSize()); } -// A more difficult test that involves missing pos_in_set and set_size values. -TEST(AXTreeTest, TestSetSizePosInSetMissingDifficult) { +// A more difficult test that involves missing PosInSet and SetSize values. +TEST(AXTreeTest, SetSizePosInSetMissingDifficult) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(6); @@ -3380,9 +3383,9 @@ TEST(AXTreeTest, TestSetSizePosInSetMissingDifficult) { EXPECT_OPTIONAL_EQ(11, item5->GetSetSize()); } -// Tests that code overwrites decreasing set_size assignments to largest of +// Tests that code overwrites decreasing SetSize assignments to largest of // assigned values. -TEST(AXTreeTest, TestSetSizeDecreasing) { +TEST(AXTreeTest, SetSizeDecreasing) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); @@ -3410,8 +3413,8 @@ TEST(AXTreeTest, TestSetSizeDecreasing) { EXPECT_OPTIONAL_EQ(5, item3->GetSetSize()); } -// Tests that code overwrites decreasing pos_in_set values. -TEST(AXTreeTest, TestPosInSetDecreasing) { +// Tests that code overwrites decreasing PosInSet values. +TEST(AXTreeTest, PosInSetDecreasing) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); @@ -3439,10 +3442,10 @@ TEST(AXTreeTest, TestPosInSetDecreasing) { EXPECT_OPTIONAL_EQ(8, item3->GetSetSize()); } -// Tests that code overwrites duplicate pos_in_set values. Note this case is +// Tests that code overwrites duplicate PosInSet values. Note this case is // tricky; an update to the second element causes an update to the third // element. -TEST(AXTreeTest, TestPosInSetDuplicates) { +TEST(AXTreeTest, PosInSetDuplicates) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); @@ -3473,7 +3476,7 @@ TEST(AXTreeTest, TestPosInSetDuplicates) { // Tests GetPosInSet and GetSetSize when some list items are nested in a generic // container. -TEST(AXTreeTest, TestSetSizePosInSetNestedContainer) { +TEST(AXTreeTest, SetSizePosInSetNestedContainer) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(7); @@ -3517,10 +3520,8 @@ TEST(AXTreeTest, TestSetSizePosInSetNestedContainer) { } // Tests GetSetSize and GetPosInSet are correct, even when list items change. -// This test is directed at the caching functionality of pos_in_set and -// set_size. Tests that previously calculated values are not used after -// tree is updated. -TEST(AXTreeTest, TestSetSizePosInSetDeleteItem) { +// Tests that previously calculated values are not used after tree is updated. +TEST(AXTreeTest, SetSizePosInSetDeleteItem) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(4); @@ -3561,9 +3562,9 @@ TEST(AXTreeTest, TestSetSizePosInSetDeleteItem) { // Tests GetSetSize and GetPosInSet are correct, even when list items change. // This test adds an item to the front of a list, which invalidates previously -// calculated pos_in_set and set_size values. Tests that old values are not +// calculated PosInSet and SetSize values. Tests that old values are not // used after tree is updated. -TEST(AXTreeTest, TestSetSizePosInSetAddItem) { +TEST(AXTreeTest, SetSizePosInSetAddItem) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(4); @@ -3611,22 +3612,22 @@ TEST(AXTreeTest, TestSetSizePosInSetAddItem) { EXPECT_OPTIONAL_EQ(4, new_item4->GetSetSize()); } -// Tests that the outerlying ordered set reports a set_size. Ordered sets -// should not report a pos_in_set value other than 0, since they are not +// Tests that the outerlying ordered set reports a SetSize. Ordered sets +// should not report a PosInSet value other than 0, since they are not // considered to be items within a set (even when nested). -TEST(AXTreeTest, TestOrderedSetReportsSetSize) { +TEST(AXTreeTest, OrderedSetReportsSetSize) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(12); tree_update.nodes[0].id = 1; - tree_update.nodes[0].role = ax::mojom::Role::kList; // set_size = 3 + tree_update.nodes[0].role = ax::mojom::Role::kList; // SetSize = 3 tree_update.nodes[0].child_ids = {2, 3, 4, 7, 8, 9, 12}; tree_update.nodes[1].id = 2; tree_update.nodes[1].role = ax::mojom::Role::kListItem; // 1 of 3 tree_update.nodes[2].id = 3; tree_update.nodes[2].role = ax::mojom::Role::kListItem; // 2 of 3 tree_update.nodes[3].id = 4; - tree_update.nodes[3].role = ax::mojom::Role::kList; // set_size = 2 + tree_update.nodes[3].role = ax::mojom::Role::kList; // SetSize = 2 tree_update.nodes[3].child_ids = {5, 6}; tree_update.nodes[4].id = 5; tree_update.nodes[4].role = ax::mojom::Role::kListItem; // 1 of 2 @@ -3635,10 +3636,10 @@ TEST(AXTreeTest, TestOrderedSetReportsSetSize) { tree_update.nodes[6].id = 7; tree_update.nodes[6].role = ax::mojom::Role::kListItem; // 3 of 3 tree_update.nodes[7].id = 8; - tree_update.nodes[7].role = ax::mojom::Role::kList; // set_size = 0 + tree_update.nodes[7].role = ax::mojom::Role::kList; // SetSize = 0 tree_update.nodes[8].id = 9; tree_update.nodes[8].role = - ax::mojom::Role::kList; // set_size = 1 because only 1 item whose role + ax::mojom::Role::kList; // SetSize = 1 because only 1 item whose role // matches tree_update.nodes[8].child_ids = {10, 11}; tree_update.nodes[9].id = 10; @@ -3682,8 +3683,8 @@ TEST(AXTreeTest, TestOrderedSetReportsSetSize) { // Only 1 item whose role matches. EXPECT_OPTIONAL_EQ(1, inner_list3->GetSetSize()); AXNode* inner_list3_article1 = tree.GetFromId(10); - EXPECT_OPTIONAL_EQ(0, inner_list3_article1->GetPosInSet()); - EXPECT_OPTIONAL_EQ(0, inner_list3_article1->GetSetSize()); + EXPECT_FALSE(inner_list3_article1->GetPosInSet()); + EXPECT_FALSE(inner_list3_article1->GetSetSize()); AXNode* inner_list3_item1 = tree.GetFromId(11); EXPECT_OPTIONAL_EQ(1, inner_list3_item1->GetPosInSet()); EXPECT_OPTIONAL_EQ(1, inner_list3_item1->GetSetSize()); @@ -3696,7 +3697,7 @@ TEST(AXTreeTest, TestOrderedSetReportsSetSize) { } // Tests GetPosInSet and GetSetSize code on invalid input. -TEST(AXTreeTest, TestSetSizePosInSetInvalid) { +TEST(AXTreeTest, SetSizePosInSetInvalid) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(3); @@ -3715,17 +3716,17 @@ TEST(AXTreeTest, TestSetSizePosInSetInvalid) { EXPECT_FALSE(item1->GetPosInSet()); EXPECT_FALSE(item1->GetSetSize()); AXNode* item2 = tree.GetFromId(2); - EXPECT_OPTIONAL_EQ(0, item2->GetPosInSet()); - EXPECT_OPTIONAL_EQ(0, item2->GetSetSize()); + EXPECT_FALSE(item2->GetPosInSet()); + EXPECT_FALSE(item2->GetSetSize()); AXNode* item3 = tree.GetFromId(3); - EXPECT_OPTIONAL_EQ(0, item3->GetPosInSet()); - EXPECT_OPTIONAL_EQ(0, item3->GetSetSize()); + EXPECT_FALSE(item3->GetPosInSet()); + EXPECT_FALSE(item3->GetSetSize()); } // Tests GetPosInSet and GetSetSize code on kRadioButtons. Radio buttons // behave differently than other item-like elements; most notably, they do not // need to be contained within an ordered set to report a PosInSet or SetSize. -TEST(AXTreeTest, TestSetSizePosInSetRadioButtons) { +TEST(AXTreeTest, SetSizePosInSetRadioButtons) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(13); @@ -3837,13 +3838,13 @@ TEST(AXTreeTest, TestSetSizePosInSetRadioButtons) { // Tests GetPosInSet and GetSetSize on a list that includes radio buttons. // Note that radio buttons do not contribute to the SetSize of the outerlying // list. -TEST(AXTreeTest, TestSetSizePosInSetRadioButtonsInList) { +TEST(AXTreeTest, SetSizePosInSetRadioButtonsInList) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(6); tree_update.nodes[0].id = 1; tree_update.nodes[0].role = - ax::mojom::Role::kList; // set_size = 2, since only contains 2 ListItems + ax::mojom::Role::kList; // SetSize = 2, since only contains 2 ListItems tree_update.nodes[0].child_ids = {2, 3, 4, 5, 6}; tree_update.nodes[1].id = 2; @@ -3888,7 +3889,7 @@ TEST(AXTreeTest, TestSetSizePosInSetRadioButtonsInList) { // to the tree representation, the three elements are siblings. However, // due to the presence of the kHierarchicalLevel attribute, they all belong // to different sets. -TEST(AXTreeTest, TestSetSizePosInSetFlatTree) { +TEST(AXTreeTest, SetSizePosInSetFlatTree) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(4); @@ -3922,7 +3923,7 @@ TEST(AXTreeTest, TestSetSizePosInSetFlatTree) { // Tests GetPosInSet and GetSetSize on a flat tree representation, where only // the level is specified. -TEST(AXTreeTest, TestSetSizePosInSetFlatTreeLevelsOnly) { +TEST(AXTreeTest, SetSizePosInSetFlatTreeLevelsOnly) { AXTreeUpdate tree_update; tree_update.root_id = 1; tree_update.nodes.resize(9); @@ -3994,7 +3995,7 @@ TEST(AXTreeTest, TestSetSizePosInSetFlatTreeLevelsOnly) { // Tests that GetPosInSet and GetSetSize work while a tree is being // unserialized. -TEST(AXTreeTest, TestSetSizePosInSetSubtreeDeleted) { +TEST(AXTreeTest, SetSizePosInSetSubtreeDeleted) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(3); @@ -4035,7 +4036,7 @@ TEST(AXTreeTest, TestSetSizePosInSetSubtreeDeleted) { } // Tests that GetPosInSet and GetSetSize work when there are ignored nodes. -TEST(AXTreeTest, TestSetSizePosInSetIgnoredItem) { +TEST(AXTreeTest, SetSizePosInSetIgnoredItem) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(3); @@ -4083,7 +4084,7 @@ TEST(AXTreeTest, TestSetSizePosInSetIgnoredItem) { // Tests that kPopUpButtons are assigned the SetSize of the wrapped // kMenuListPopup, if one is present. -TEST(AXTreeTest, TestSetSizePosInSetPopUpButton) { +TEST(AXTreeTest, SetSizePosInSetPopUpButton) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(6); @@ -4114,7 +4115,7 @@ TEST(AXTreeTest, TestSetSizePosInSetPopUpButton) { // Tests that PosInSet and SetSize are still correctly calculated when there // are nodes with role of kUnknown layered between items and ordered set. -TEST(AXTreeTest, TestSetSizePosInSetUnkown) { +TEST(AXTreeTest, SetSizePosInSetUnkown) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(5); @@ -4143,7 +4144,7 @@ TEST(AXTreeTest, TestSetSizePosInSetUnkown) { EXPECT_OPTIONAL_EQ(2, item2->GetSetSize()); } -TEST(AXTreeTest, TestSetSizePosInSetMenuItemValidChildOfMenuListPopup) { +TEST(AXTreeTest, SetSizePosInSetMenuItemValidChildOfMenuListPopup) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(3); @@ -4166,7 +4167,7 @@ TEST(AXTreeTest, TestSetSizePosInSetMenuItemValidChildOfMenuListPopup) { EXPECT_OPTIONAL_EQ(2, item2->GetSetSize()); } -TEST(AXTreeTest, TestSetSizePostInSetListBoxOptionWithGroup) { +TEST(AXTreeTest, SetSizePostInSetListBoxOptionWithGroup) { AXTreeUpdate initial_state; initial_state.root_id = 1; initial_state.nodes.resize(7); @@ -4191,16 +4192,212 @@ TEST(AXTreeTest, TestSetSizePostInSetListBoxOptionWithGroup) { AXNode* listbox_option1 = tree.GetFromId(4); EXPECT_OPTIONAL_EQ(1, listbox_option1->GetPosInSet()); - EXPECT_OPTIONAL_EQ(2, listbox_option1->GetSetSize()); + EXPECT_OPTIONAL_EQ(4, listbox_option1->GetSetSize()); AXNode* listbox_option2 = tree.GetFromId(5); EXPECT_OPTIONAL_EQ(2, listbox_option2->GetPosInSet()); - EXPECT_OPTIONAL_EQ(2, listbox_option2->GetSetSize()); + EXPECT_OPTIONAL_EQ(4, listbox_option2->GetSetSize()); AXNode* listbox_option3 = tree.GetFromId(6); - EXPECT_OPTIONAL_EQ(1, listbox_option3->GetPosInSet()); - EXPECT_OPTIONAL_EQ(2, listbox_option3->GetSetSize()); + EXPECT_OPTIONAL_EQ(3, listbox_option3->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, listbox_option3->GetSetSize()); AXNode* listbox_option4 = tree.GetFromId(7); - EXPECT_OPTIONAL_EQ(2, listbox_option4->GetPosInSet()); - EXPECT_OPTIONAL_EQ(2, listbox_option4->GetSetSize()); + EXPECT_OPTIONAL_EQ(4, listbox_option4->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, listbox_option4->GetSetSize()); +} + +TEST(AXTreeTest, SetSizePosInSetGroup) { + // The behavior of a group changes depending on the context it appears in + // i.e. if it appears alone vs. if it is contained within another set-like + // element. The below example shows a group standing alone: + // + // <ul role="group"> <!-- SetSize = 3 --> + // <li role="menuitemradio" aria-checked="true">Small</li> + // <li role="menuitemradio" aria-checked="false">Medium</li> + // <li role="menuitemradio" aria-checked="false">Large</li> + // </ul> + // + // However, when it is contained within another set-like element, like a + // listbox, it should simply act like a generic container: + // + // <div role="listbox"> <!-- SetSize = 3 --> + // <div role="option">Red</div> <!-- 1 of 3 --> + // <div role="option">Yellow</div> <!-- 2 of 3 --> + // <div role="group"> <!-- SetSize = 0 --> + // <div role="option">Blue</div> <!-- 3 of 3 --> + // </div> + // </div> + // + // Please note: the GetPosInSet and GetSetSize functions take slightly + // different code paths when initially run on items vs. the container. + // Exercise both code paths in this test. + + AXTreeUpdate tree_update; + tree_update.root_id = 1; + tree_update.nodes.resize(6); + tree_update.nodes[0].id = 1; + tree_update.nodes[0].role = ax::mojom::Role::kMenu; // SetSize = 4 + tree_update.nodes[0].child_ids = {2, 6}; + tree_update.nodes[1].id = 2; + tree_update.nodes[1].role = ax::mojom::Role::kGroup; // SetSize = 0 + tree_update.nodes[1].child_ids = {3, 4, 5}; + tree_update.nodes[2].id = 3; + tree_update.nodes[2].role = ax::mojom::Role::kMenuItemRadio; // 1 of 4 + tree_update.nodes[3].id = 4; + tree_update.nodes[3].role = ax::mojom::Role::kMenuItemRadio; // 2 of 4 + tree_update.nodes[4].id = 5; + tree_update.nodes[4].role = ax::mojom::Role::kMenuItemRadio; // 3 of 4 + tree_update.nodes[5].id = 6; + tree_update.nodes[5].role = ax::mojom::Role::kMenuItemRadio; // 4 of 4 + AXTree tree(tree_update); + + // Get data on kMenu first. + AXNode* menu = tree.GetFromId(1); + EXPECT_OPTIONAL_EQ(4, menu->GetSetSize()); + AXNode* group = tree.GetFromId(2); + EXPECT_FALSE(group->GetSetSize()); + // The below values should have already been computed and cached. + AXNode* item1 = tree.GetFromId(3); + EXPECT_OPTIONAL_EQ(1, item1->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, item1->GetSetSize()); + AXNode* item4 = tree.GetFromId(6); + EXPECT_OPTIONAL_EQ(4, item4->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, item4->GetSetSize()); + + AXTreeUpdate next_tree_update; + next_tree_update.root_id = 1; + next_tree_update.nodes.resize(6); + next_tree_update.nodes[0].id = 1; + next_tree_update.nodes[0].role = ax::mojom::Role::kListBox; // SetSize = 4 + next_tree_update.nodes[0].child_ids = {2, 6}; + next_tree_update.nodes[1].id = 2; + next_tree_update.nodes[1].role = ax::mojom::Role::kGroup; // SetSize = 0 + next_tree_update.nodes[1].child_ids = {3, 4, 5}; + next_tree_update.nodes[2].id = 3; + next_tree_update.nodes[2].role = ax::mojom::Role::kListBoxOption; // 1 of 4 + next_tree_update.nodes[3].id = 4; + next_tree_update.nodes[3].role = ax::mojom::Role::kListBoxOption; // 2 of 4 + next_tree_update.nodes[4].id = 5; + next_tree_update.nodes[4].role = ax::mojom::Role::kListBoxOption; // 3 of 4 + next_tree_update.nodes[5].id = 6; + next_tree_update.nodes[5].role = ax::mojom::Role::kListBoxOption; // 4 of 4 + AXTree next_tree(next_tree_update); + + // Get data on kListBoxOption first. + AXNode* option1 = next_tree.GetFromId(3); + EXPECT_OPTIONAL_EQ(1, option1->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, option1->GetSetSize()); + AXNode* option2 = next_tree.GetFromId(4); + EXPECT_OPTIONAL_EQ(2, option2->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, option2->GetSetSize()); + AXNode* option3 = next_tree.GetFromId(5); + EXPECT_OPTIONAL_EQ(3, option3->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, option3->GetSetSize()); + AXNode* option4 = next_tree.GetFromId(6); + EXPECT_OPTIONAL_EQ(4, option4->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, option4->GetSetSize()); + AXNode* next_group = next_tree.GetFromId(2); + EXPECT_FALSE(next_group->GetSetSize()); + // The below value should have already been computed and cached. + AXNode* listbox = next_tree.GetFromId(1); + EXPECT_OPTIONAL_EQ(4, listbox->GetSetSize()); + + // Standalone groups are allowed. + AXTreeUpdate third_tree_update; + third_tree_update.root_id = 1; + third_tree_update.nodes.resize(3); + third_tree_update.nodes[0].id = 1; + third_tree_update.nodes[0].role = ax::mojom::Role::kGroup; + third_tree_update.nodes[0].child_ids = {2, 3}; + third_tree_update.nodes[1].id = 2; + third_tree_update.nodes[1].role = ax::mojom::Role::kListItem; + third_tree_update.nodes[2].id = 3; + third_tree_update.nodes[2].role = ax::mojom::Role::kListItem; + AXTree third_tree(third_tree_update); + + // Ensure that groups can't also stand alone. + AXNode* last_group = third_tree.GetFromId(1); + EXPECT_OPTIONAL_EQ(2, last_group->GetSetSize()); + AXNode* list_item1 = third_tree.GetFromId(2); + EXPECT_OPTIONAL_EQ(1, list_item1->GetPosInSet()); + EXPECT_OPTIONAL_EQ(2, list_item1->GetSetSize()); + AXNode* list_item2 = third_tree.GetFromId(3); + EXPECT_OPTIONAL_EQ(2, list_item2->GetPosInSet()); + EXPECT_OPTIONAL_EQ(2, list_item2->GetSetSize()); + + // Test nested groups. + AXTreeUpdate last_tree_update; + last_tree_update.root_id = 1; + last_tree_update.nodes.resize(6); + last_tree_update.nodes[0].id = 1; + last_tree_update.nodes[0].role = ax::mojom::Role::kMenuBar; + last_tree_update.nodes[0].child_ids = {2}; + last_tree_update.nodes[1].id = 2; + last_tree_update.nodes[1].role = ax::mojom::Role::kGroup; + last_tree_update.nodes[1].child_ids = {3, 4}; + last_tree_update.nodes[2].id = 3; + last_tree_update.nodes[2].role = ax::mojom::Role::kMenuItemCheckBox; + last_tree_update.nodes[3].id = 4; + last_tree_update.nodes[3].role = ax::mojom::Role::kGroup; + last_tree_update.nodes[3].child_ids = {5, 6}; + last_tree_update.nodes[4].id = 5; + last_tree_update.nodes[4].role = ax::mojom::Role::kMenuItemCheckBox; + last_tree_update.nodes[5].id = 6; + last_tree_update.nodes[5].role = ax::mojom::Role::kMenuItemCheckBox; + AXTree last_tree(last_tree_update); + + AXNode* checkbox1 = last_tree.GetFromId(3); + EXPECT_OPTIONAL_EQ(1, checkbox1->GetPosInSet()); + EXPECT_OPTIONAL_EQ(3, checkbox1->GetSetSize()); + AXNode* checkbox2 = last_tree.GetFromId(5); + EXPECT_OPTIONAL_EQ(2, checkbox2->GetPosInSet()); + EXPECT_OPTIONAL_EQ(3, checkbox2->GetSetSize()); + AXNode* checkbox3 = last_tree.GetFromId(6); + EXPECT_OPTIONAL_EQ(3, checkbox3->GetPosInSet()); + EXPECT_OPTIONAL_EQ(3, checkbox3->GetSetSize()); + AXNode* menu_bar = last_tree.GetFromId(1); + EXPECT_OPTIONAL_EQ(3, menu_bar->GetSetSize()); + AXNode* outer_group = last_tree.GetFromId(2); + EXPECT_FALSE(outer_group->GetSetSize()); + AXNode* inner_group = last_tree.GetFromId(4); + EXPECT_FALSE(inner_group->GetSetSize()); +} + +TEST(AXTreeTest, SetSizePosInSetHidden) { + AXTreeUpdate tree_update; + tree_update.root_id = 1; + tree_update.nodes.resize(6); + tree_update.nodes[0].id = 1; + tree_update.nodes[0].role = ax::mojom::Role::kListBox; // SetSize = 4 + tree_update.nodes[0].child_ids = {2, 3, 4, 5, 6}; + tree_update.nodes[1].id = 2; + tree_update.nodes[1].role = ax::mojom::Role::kListBoxOption; // 1 of 4 + tree_update.nodes[2].id = 3; + tree_update.nodes[2].role = ax::mojom::Role::kListBoxOption; // 2 of 4 + tree_update.nodes[3].id = 4; + tree_update.nodes[3].role = ax::mojom::Role::kListBoxOption; // Hidden + tree_update.nodes[3].AddState(ax::mojom::State::kInvisible); + tree_update.nodes[4].id = 5; + tree_update.nodes[4].role = ax::mojom::Role::kListBoxOption; // 3 of 4 + tree_update.nodes[5].id = 6; + tree_update.nodes[5].role = ax::mojom::Role::kListBoxOption; // 4 of 4 + AXTree tree(tree_update); + + AXNode* list_box = tree.GetFromId(1); + EXPECT_OPTIONAL_EQ(4, list_box->GetSetSize()); + AXNode* option1 = tree.GetFromId(2); + EXPECT_OPTIONAL_EQ(1, option1->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, option1->GetSetSize()); + AXNode* option2 = tree.GetFromId(3); + EXPECT_OPTIONAL_EQ(2, option2->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, option2->GetSetSize()); + AXNode* option_hidden = tree.GetFromId(4); + EXPECT_FALSE(option_hidden->GetPosInSet()); + EXPECT_FALSE(option_hidden->GetSetSize()); + AXNode* option3 = tree.GetFromId(5); + EXPECT_OPTIONAL_EQ(3, option3->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, option3->GetSetSize()); + AXNode* option4 = tree.GetFromId(6); + EXPECT_OPTIONAL_EQ(4, option4->GetPosInSet()); + EXPECT_OPTIONAL_EQ(4, option4->GetSetSize()); } TEST(AXTreeTest, OnNodeWillBeDeletedHasValidUnignoredParent) { diff --git a/chromium/ui/accessibility/ax_tree_update.h b/chromium/ui/accessibility/ax_tree_update.h index 5225cdefb87..a27fc3b2377 100644 --- a/chromium/ui/accessibility/ax_tree_update.h +++ b/chromium/ui/accessibility/ax_tree_update.h @@ -13,7 +13,9 @@ #include <vector> #include "base/strings/string_number_conversions.h" +#include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_event_intent.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_tree_data.h" @@ -72,9 +74,12 @@ template<typename AXNodeData, typename AXTreeData> struct AXTreeUpdateBase { // A vector of nodes to update, according to the rules above. std::vector<AXNodeData> nodes; - // The source of the event. + // The source of the event which generated this tree update. ax::mojom::EventFrom event_from = ax::mojom::EventFrom::kNone; + // The event intents associated with this tree update. + std::vector<AXEventIntent> event_intents; + // Return a multi-line indented string representation, for logging. std::string ToString() const; @@ -100,6 +105,16 @@ std::string AXTreeUpdateBase<AXNodeData, AXTreeData>::ToString() const { result += "AXTreeUpdate: root id " + base::NumberToString(root_id) + "\n"; } + if (event_from != ax::mojom::EventFrom::kNone) + result += "event_from=" + std::string(ui::ToString(event_from)) + "\n"; + + if (!event_intents.empty()) { + result += "event_intents=[\n"; + for (const auto& event_intent : event_intents) + result += " " + event_intent.ToString() + "\n"; + result += "]\n"; + } + // The challenge here is that we want to indent the nodes being updated // so that parent/child relationships are clear, but we don't have access // to the rest of the tree for context, so we have to try to show the diff --git a/chromium/ui/accessibility/extensions/chromevoxclassic/BUILD.gn b/chromium/ui/accessibility/extensions/chromevoxclassic/BUILD.gn index a2f3cec09e6..dfecb666555 100644 --- a/chromium/ui/accessibility/extensions/chromevoxclassic/BUILD.gn +++ b/chromium/ui/accessibility/extensions/chromevoxclassic/BUILD.gn @@ -4,7 +4,7 @@ import("//build/config/features.gni") import( - "//chrome/browser/resources/chromeos/accessibility/chromevox/run_jsbundler.gni") + "//chrome/browser/resources/chromeos/accessibility/common/run_jsbundler.gni") import( "//chrome/browser/resources/chromeos/accessibility/strings/accessibility_strings.gni") import("//chrome/common/features.gni") @@ -586,8 +586,8 @@ js2gtest("chromevox_unitjs_tests") { "walkers/word_walker_test.unitjs", ] gen_include_files = [ - "//chrome/browser/resources/chromeos/accessibility/chromevox/testing/assert_additions.js", - "//chrome/browser/resources/chromeos/accessibility/chromevox/testing/callback_helper.js", + "//chrome/browser/resources/chromeos/accessibility/common/testing/assert_additions.js", + "//chrome/browser/resources/chromeos/accessibility/common/testing/callback_helper.js", "//ui/accessibility/extensions/chromevoxclassic/testing/chromevox_unittest_base.js", ] diff --git a/chromium/ui/accessibility/extensions/chromevoxclassic/testing/chromevox_unittest_base.js b/chromium/ui/accessibility/extensions/chromevoxclassic/testing/chromevox_unittest_base.js index 29e01eb3824..01fcc5b6bdb 100644 --- a/chromium/ui/accessibility/extensions/chromevoxclassic/testing/chromevox_unittest_base.js +++ b/chromium/ui/accessibility/extensions/chromevoxclassic/testing/chromevox_unittest_base.js @@ -3,11 +3,14 @@ // found in the LICENSE file. GEN_INCLUDE([ - '//chrome/browser/resources/chromeos/accessibility/chromevox/testing/assert_additions.js' + '//chrome/browser/resources/chromeos/accessibility/common/testing/' + + 'assert_additions.js' ]); GEN_INCLUDE([ - '//chrome/browser/resources/chromeos/accessibility/chromevox/testing/common.js', - '//chrome/browser/resources/chromeos/accessibility/chromevox/testing/callback_helper.js' + '//chrome/browser/resources/chromeos/accessibility/chromevox/testing/' + + 'common.js', + '//chrome/browser/resources/chromeos/accessibility/common/testing/' + + 'callback_helper.js' ]); /** diff --git a/chromium/ui/accessibility/mojom/ax_tree_update.mojom b/chromium/ui/accessibility/mojom/ax_tree_update.mojom index 495f88158ea..957edd556c0 100644 --- a/chromium/ui/accessibility/mojom/ax_tree_update.mojom +++ b/chromium/ui/accessibility/mojom/ax_tree_update.mojom @@ -5,6 +5,7 @@ module ax.mojom; import "ui/accessibility/ax_enums.mojom"; +import "ui/accessibility/mojom/ax_event_intent.mojom"; import "ui/accessibility/mojom/ax_node_data.mojom"; import "ui/accessibility/mojom/ax_tree_data.mojom"; @@ -16,4 +17,5 @@ struct AXTreeUpdate { int32 root_id; array<AXNodeData> nodes; ax.mojom.EventFrom event_from; + array<EventIntent> event_intents; }; diff --git a/chromium/ui/accessibility/mojom/ax_tree_update_mojom_traits.cc b/chromium/ui/accessibility/mojom/ax_tree_update_mojom_traits.cc index 159aa36b138..e429ce978d3 100644 --- a/chromium/ui/accessibility/mojom/ax_tree_update_mojom_traits.cc +++ b/chromium/ui/accessibility/mojom/ax_tree_update_mojom_traits.cc @@ -18,7 +18,7 @@ bool StructTraits<ax::mojom::AXTreeUpdateDataView, ui::AXTreeUpdate>::Read( if (!data.ReadNodes(&out->nodes)) return false; out->event_from = data.event_from(); - return true; + return data.ReadEventIntents(&out->event_intents); } } // namespace mojo diff --git a/chromium/ui/accessibility/mojom/ax_tree_update_mojom_traits.h b/chromium/ui/accessibility/mojom/ax_tree_update_mojom_traits.h index 18a8bb8af1e..480c07aca8f 100644 --- a/chromium/ui/accessibility/mojom/ax_tree_update_mojom_traits.h +++ b/chromium/ui/accessibility/mojom/ax_tree_update_mojom_traits.h @@ -5,7 +5,10 @@ #ifndef UI_ACCESSIBILITY_MOJOM_AX_TREE_UPDATE_MOJOM_TRAITS_H_ #define UI_ACCESSIBILITY_MOJOM_AX_TREE_UPDATE_MOJOM_TRAITS_H_ +#include "ui/accessibility/ax_event_intent.h" #include "ui/accessibility/ax_tree_update.h" +#include "ui/accessibility/mojom/ax_event_intent.mojom.h" +#include "ui/accessibility/mojom/ax_event_intent_mojom_traits.h" #include "ui/accessibility/mojom/ax_node_data_mojom_traits.h" #include "ui/accessibility/mojom/ax_tree_data_mojom_traits.h" #include "ui/accessibility/mojom/ax_tree_update.mojom-shared.h" @@ -30,6 +33,10 @@ struct StructTraits<ax::mojom::AXTreeUpdateDataView, ui::AXTreeUpdate> { static ax::mojom::EventFrom event_from(const ui::AXTreeUpdate& p) { return p.event_from; } + static std::vector<ui::AXEventIntent> event_intents( + const ui::AXTreeUpdate& p) { + return p.event_intents; + } static bool Read(ax::mojom::AXTreeUpdateDataView data, ui::AXTreeUpdate* out); }; diff --git a/chromium/ui/accessibility/platform/BUILD.gn b/chromium/ui/accessibility/platform/BUILD.gn new file mode 100644 index 00000000000..691a701457d --- /dev/null +++ b/chromium/ui/accessibility/platform/BUILD.gn @@ -0,0 +1,142 @@ +# Copyright 2020 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. + +import("//build/config/features.gni") +import("//build/config/jumbo.gni") +import("//build/config/linux/pkg_config.gni") +import("//build/config/ui.gni") +import("//mojo/public/tools/bindings/mojom.gni") +import("//testing/libfuzzer/fuzzer_test.gni") +import("//testing/test.gni") +import("//tools/json_schema_compiler/json_schema_api.gni") +import("//ui/base/ui_features.gni") + +if (is_win) { + import("//build/toolchain/win/midl.gni") +} + +if (is_win) { + midl("ichromeaccessible") { + sources = [ "ichromeaccessible.idl" ] + } +} + +if (is_android) { + import("//build/config/android/rules.gni") +} + +source_set("platform") { + defines = [ "AX_IMPLEMENTATION" ] + + visibility = [ "//ui/accessibility" ] + + sources = [ + # Used by by browser_accessibility_state_impl.cc. + "ax_platform_node.cc", + "ax_platform_node.h", + "ax_platform_node_delegate.h", + + # Used by browser_accessibility.cc. + "ax_unique_id.cc", + "ax_unique_id.h", + + # Used by accessibility_tree_formatter_blink.cc. + "compute_attributes.cc", + "compute_attributes.h", + + # Used by //ui/accessibility:ax_assistant. + "ax_android_constants.cc", + "ax_android_constants.h", + + # Used by //ui/views/views/ax_virtual_view.h. + "ax_platform_node_base.cc", + "ax_platform_node_base.h", + "ax_platform_node_delegate_base.cc", + "ax_platform_node_delegate_base.h", + + # Used by //chrome/test/browser_tests/browser_view_browsertest.cc + "ax_platform_node_test_helper.cc", + "ax_platform_node_test_helper.h", + ] + + public_deps = [ + "//ui/accessibility:ax_base", + "//ui/display", + ] + + if (has_native_accessibility) { + sources += [ + "ax_fragment_root_delegate_win.h", + "ax_fragment_root_win.cc", + "ax_fragment_root_win.h", + "ax_platform_node_delegate_utils_win.cc", + "ax_platform_node_delegate_utils_win.h", + "ax_platform_node_textchildprovider_win.cc", + "ax_platform_node_textchildprovider_win.h", + "ax_platform_node_textprovider_win.cc", + "ax_platform_node_textprovider_win.h", + "ax_platform_node_textrangeprovider_win.cc", + "ax_platform_node_textrangeprovider_win.h", + "ax_platform_node_win.cc", + "ax_platform_node_win.h", + "ax_platform_relation_win.cc", + "ax_platform_relation_win.h", + "ax_platform_text_boundary.cc", + "ax_platform_text_boundary.h", + "ax_system_caret_win.cc", + "ax_system_caret_win.h", + "uia_registrar_win.cc", + "uia_registrar_win.h", + ] + + if (is_win) { + public_deps += [ + "//third_party/iaccessible2", + "//ui/accessibility/platform:ichromeaccessible", + ] + + libs = [ + "oleacc.lib", + "uiautomationcore.lib", + ] + } + + if (is_mac) { + sources += [ + "ax_platform_node_mac.h", + "ax_platform_node_mac.mm", + ] + + libs = [ + "AppKit.framework", + "Foundation.framework", + ] + } + + if (use_atk) { + sources += [ + "atk_util_auralinux.cc", + "atk_util_auralinux.h", + "atk_util_auralinux_gtk.cc", + "ax_platform_atk_hyperlink.cc", + "ax_platform_atk_hyperlink.h", + "ax_platform_node_auralinux.cc", + "ax_platform_node_auralinux.h", + ] + + # ax_platform_text_boundary.h includes atk.h, so ATK is needed + # as a public config to ensure anything that includes this is + # able to find atk.h. + public_configs = [ "//build/config/linux/atk" ] + + if (use_glib) { + configs += [ "//build/config/linux:glib" ] + } + + if (use_x11) { + public_deps += [ "//ui/gfx/x" ] + } + } + } +} diff --git a/chromium/ui/accessibility/platform/ax_fragment_root_win.cc b/chromium/ui/accessibility/platform/ax_fragment_root_win.cc index f1925832840..163fb8446dd 100644 --- a/chromium/ui/accessibility/platform/ax_fragment_root_win.cc +++ b/chromium/ui/accessibility/platform/ax_fragment_root_win.cc @@ -7,17 +7,21 @@ #include <unordered_map> #include "base/no_destructor.h" +#include "base/strings/string_number_conversions.h" #include "ui/accessibility/platform/ax_fragment_root_delegate_win.h" #include "ui/accessibility/platform/ax_platform_node_win.h" +#include "ui/accessibility/platform/uia_registrar_win.h" #include "ui/base/win/atl_module.h" namespace ui { class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, + public IItemContainerProvider, public IRawElementProviderFragmentRoot, public IRawElementProviderAdviseEvents { public: BEGIN_COM_MAP(AXFragmentRootPlatformNodeWin) + COM_INTERFACE_ENTRY(IItemContainerProvider) COM_INTERFACE_ENTRY(IRawElementProviderFragmentRoot) COM_INTERFACE_ENTRY(IRawElementProviderAdviseEvents) COM_INTERFACE_ENTRY_CHAIN(AXPlatformNodeWin) @@ -38,19 +42,76 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, } // + // IItemContainerProvider methods. + // + IFACEMETHODIMP FindItemByProperty( + IRawElementProviderSimple* start_after_element, + PROPERTYID property_id, + VARIANT value, + IRawElementProviderSimple** result) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ITEMCONTAINER_FINDITEMBYPROPERTY); + UIA_VALIDATE_CALL_1_ARG(result); + *result = nullptr; + + // We currently only support the custom UIA property ID for unique id and we + // ignore |start_after_element|. + if (property_id == + UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId() && + value.vt == VT_BSTR) { + // TODO: We should support the case when |start_after_element| isn't + // nullptr for unique id (https://crbug.com/1098160). + if (start_after_element) + return E_INVALIDARG; + + int32_t ax_unique_id; + if (!base::StringToInt(value.bstrVal, &ax_unique_id)) + return S_OK; + + // In the Windows accessibility platform implementation, id 0 represents + // self; a positive id represents the immediate descendants; and a + // negative id represents a unique id that can be mapped to any node. + if (AXPlatformNodeWin* node_win = + static_cast<AXPlatformNodeWin*>(GetFromUniqueId(-ax_unique_id))) { + node_win->QueryInterface(IID_PPV_ARGS(result)); + } + + return S_OK; + } + + return E_INVALIDARG; + } + + // // IRawElementProviderSimple methods. // IFACEMETHODIMP get_HostRawElementProvider( IRawElementProviderSimple** host_element_provider) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_HOST_RAW_ELEMENT_PROVIDER); UIA_VALIDATE_CALL_1_ARG(host_element_provider); HWND hwnd = GetDelegate()->GetTargetForNativeAccessibilityEvent(); return UiaHostProviderFromHwnd(hwnd, host_element_provider); } + IFACEMETHODIMP GetPatternProvider(PATTERNID pattern_id, + IUnknown** result) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PATTERN_PROVIDER); + UIA_VALIDATE_CALL_1_ARG(result); + *result = nullptr; + + if (pattern_id == UIA_ItemContainerPatternId) { + AddRef(); + *result = static_cast<IItemContainerProvider*>(this); + return S_OK; + } + + return AXPlatformNodeWin::GetPatternProviderImpl(pattern_id, result); + } + IFACEMETHODIMP GetPropertyValue(PROPERTYID property_id, VARIANT* result) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PROPERTY_VALUE); UIA_VALIDATE_CALL_1_ARG(result); switch (property_id) { @@ -84,6 +145,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, IFACEMETHODIMP get_FragmentRoot( IRawElementProviderFragmentRoot** fragment_root) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_FRAGMENTROOT); UIA_VALIDATE_CALL_1_ARG(fragment_root); QueryInterface(IID_PPV_ARGS(fragment_root)); @@ -97,6 +159,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, double screen_physical_pixel_x, double screen_physical_pixel_y, IRawElementProviderFragment** element_provider) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ELEMENT_PROVIDER_FROM_POINT); UIA_VALIDATE_CALL_1_ARG(element_provider); *element_provider = nullptr; @@ -124,6 +187,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, } IFACEMETHODIMP GetFocus(IRawElementProviderFragment** focus) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_FOCUS); UIA_VALIDATE_CALL_1_ARG(focus); *focus = nullptr; @@ -157,6 +221,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, // IFACEMETHODIMP AdviseEventAdded(EVENTID event_id, SAFEARRAY* property_ids) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ADVISE_EVENT_ADDED); if (event_id == UIA_LiveRegionChangedEventId) { live_region_change_listeners_++; @@ -179,6 +244,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, IFACEMETHODIMP AdviseEventRemoved(EVENTID event_id, SAFEARRAY* property_ids) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ADVISE_EVENT_REMOVED); if (event_id == UIA_LiveRegionChangedEventId) { DCHECK(live_region_change_listeners_ > 0); live_region_change_listeners_--; diff --git a/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc b/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc index 7691d12cece..e26ad0446c1 100644 --- a/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "ui/accessibility/platform/ax_fragment_root_win.h" +#include "ui/accessibility/accessibility_switches.h" #include "ui/accessibility/platform/ax_platform_node_win.h" #include "ui/accessibility/platform/ax_platform_node_win_unittest.h" #include "ui/accessibility/platform/test_ax_node_wrapper.h" @@ -14,11 +15,26 @@ #include "base/win/scoped_safearray.h" #include "base/win/scoped_variant.h" #include "testing/gtest/include/gtest/gtest.h" +#include "ui/accessibility/platform/uia_registrar_win.h" +using base::win::ScopedVariant; using Microsoft::WRL::ComPtr; namespace ui { +#define EXPECT_UIA_BSTR_EQ(node, property_id, expected) \ + { \ + ScopedVariant expectedVariant(expected); \ + ASSERT_EQ(VT_BSTR, expectedVariant.type()); \ + ASSERT_NE(nullptr, expectedVariant.ptr()->bstrVal); \ + ScopedVariant actual; \ + ASSERT_HRESULT_SUCCEEDED( \ + node->GetPropertyValue(property_id, actual.Receive())); \ + ASSERT_EQ(VT_BSTR, actual.type()); \ + ASSERT_NE(nullptr, actual.ptr()->bstrVal); \ + EXPECT_STREQ(expectedVariant.ptr()->bstrVal, actual.ptr()->bstrVal); \ + } + class AXFragmentRootTest : public AXPlatformNodeWinTest { public: AXFragmentRootTest() = default; @@ -27,6 +43,92 @@ class AXFragmentRootTest : public AXPlatformNodeWinTest { AXFragmentRootTest& operator=(const AXFragmentRootTest&) = delete; }; +TEST_F(AXFragmentRootTest, UIAFindItemByProperty) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.SetName("root"); + root.child_ids = {2, 3}; + + AXNodeData text1; + text1.id = 2; + text1.role = ax::mojom::Role::kStaticText; + text1.SetName("text1"); + + AXNodeData button; + button.id = 3; + button.role = ax::mojom::Role::kButton; + button.SetName("button"); + button.child_ids = {4}; + + AXNodeData text2; + text2.id = 4; + text2.role = ax::mojom::Role::kStaticText; + text2.SetName("text2"); + + Init(root, text1, button, text2); + InitFragmentRoot(); + + ComPtr<IRawElementProviderSimple> raw_element_provider_simple; + ax_fragment_root_->GetNativeViewAccessible()->QueryInterface( + IID_PPV_ARGS(&raw_element_provider_simple)); + + ComPtr<IItemContainerProvider> item_container_provider; + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_ItemContainerPatternId, &item_container_provider)); + ASSERT_NE(nullptr, item_container_provider.Get()); + + // Fetch the AxUniqueId of "root", and verify we can retrieve its + // corresponding IRawElementProviderSimple through FindItemByProperty(). + ScopedVariant unique_id_variant; + int32_t unique_id = AXPlatformNodeFromNode(GetRootAsAXNode())->GetUniqueId(); + unique_id_variant.Set( + SysAllocString(base::NumberToString16(-unique_id).c_str())); + ComPtr<IRawElementProviderSimple> result; + EXPECT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + unique_id_variant, &result)); + EXPECT_UIA_BSTR_EQ(result, UIA_NamePropertyId, L"root"); + result.Reset(); + unique_id_variant.Release(); + + // Fetch the AxUniqueId of "text1", and verify we can retrieve its + // corresponding IRawElementProviderSimple through FindItemByProperty(). + unique_id = + AXPlatformNodeFromNode(GetRootAsAXNode()->children()[0])->GetUniqueId(); + unique_id_variant.Set( + SysAllocString(base::NumberToString16(-unique_id).c_str())); + EXPECT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + unique_id_variant, &result)); + EXPECT_UIA_BSTR_EQ(result, UIA_NamePropertyId, L"text1"); + result.Reset(); + unique_id_variant.Release(); + + // Fetch the AxUniqueId of "button", and verify we can retrieve its + // corresponding IRawElementProviderSimple through FindItemByProperty(). + AXNode* button_node = GetRootAsAXNode()->children()[1]; + unique_id = AXPlatformNodeFromNode(button_node)->GetUniqueId(); + unique_id_variant.Set( + SysAllocString(base::NumberToString16(-unique_id).c_str())); + EXPECT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + unique_id_variant, &result)); + EXPECT_UIA_BSTR_EQ(result, UIA_NamePropertyId, L"button"); + result.Reset(); + unique_id_variant.Release(); + + // Fetch the AxUniqueId of "text2", and verify we can retrieve its + // corresponding IRawElementProviderSimple through FindItemByProperty(). + unique_id = AXPlatformNodeFromNode(button_node->children()[0])->GetUniqueId(); + unique_id_variant.Set( + SysAllocString(base::NumberToString16(-unique_id).c_str())); + EXPECT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + unique_id_variant, &result)); + EXPECT_UIA_BSTR_EQ(result, UIA_NamePropertyId, L"text2"); +} + TEST_F(AXFragmentRootTest, TestUIAGetFragmentRoot) { AXNodeData root; Init(root); @@ -227,7 +329,7 @@ TEST_F(AXFragmentRootTest, TestGetPropertyValue) { // IsControlElement and IsContentElement should follow the setting on the // fragment root delegate. test_fragment_root_delegate_->is_control_element_ = true; - base::win::ScopedVariant result; + ScopedVariant result; EXPECT_HRESULT_SUCCEEDED(root_provider->GetPropertyValue( UIA_IsControlElementPropertyId, result.Receive())); EXPECT_EQ(result.type(), VT_BOOL); diff --git a/chromium/ui/accessibility/platform/ax_platform_node.h b/chromium/ui/accessibility/platform/ax_platform_node.h index c5198fc792e..9621fcb9a5c 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node.h +++ b/chromium/ui/accessibility/platform/ax_platform_node.h @@ -92,7 +92,7 @@ class AX_EXPORT AXPlatformNode { // Return true if this object is equal to or a descendant of |ancestor|. virtual bool IsDescendantOf(AXPlatformNode* ancestor) const = 0; - // Return the unique ID + // Return the unique ID. int32_t GetUniqueId() const; // Creates a string representation of this node's data. diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc index 89309020096..a4554aaac25 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc @@ -17,6 +17,7 @@ #include "base/command_line.h" #include "base/compiler_specific.h" #include "base/debug/leak_annotations.h" +#include "base/metrics/histogram_macros.h" #include "base/no_destructor.h" #include "base/numerics/ranges.h" #include "base/optional.h" @@ -73,6 +74,29 @@ namespace ui { namespace { +// IMPORTANT! +// These values are written to logs. Do not renumber or delete +// existing items; add new entries to the end of the list. +enum class UmaAtkApi { + kGetName = 0, + kGetDescription = 1, + kGetNChildren = 2, + kRefChild = 3, + kGetIndexInParent = 4, + kGetParent = 5, + kRefRelationSet = 6, + kGetAttributes = 7, + kGetRole = 8, + kRefStateSet = 9, + // This must always be the last enum. It's okay for its value to + // increase, but none of the other enum values may change. + kMaxValue = kRefStateSet, +}; + +void RecordAccessibilityAtkApi(UmaAtkApi enum_value) { + UMA_HISTOGRAM_ENUMERATION("Accessibility.ATK-APIs", enum_value); +} + // When accepting input from clients calling the API, an ATK character offset // of -1 can often represent the length of the string. static const int kStringLengthOffset = -1; @@ -186,13 +210,17 @@ void SetIntPointerValueIfNotNull(int* pointer, int value) { *pointer = value; } +#if defined(ATK_230) bool SupportsAtkComponentScrollingInterface() { return dlsym(RTLD_DEFAULT, "atk_component_scroll_to_point"); } +#endif +#if defined(ATK_232) bool SupportsAtkTextScrollingInterface() { return dlsym(RTLD_DEFAULT, "atk_text_scroll_substring_to_point"); } +#endif AtkObject* FindAtkObjectParentFrame(AtkObject* atk_object) { AXPlatformNodeAuraLinux* node = @@ -1246,6 +1274,7 @@ char* GetStringAtOffset(AtkText* atk_text, } #endif +#if defined(ATK_230) gfx::Rect GetUnclippedParentHypertextRangeBoundsRect( AXPlatformNodeDelegate* ax_platform_node_delegate, const int start_offset, @@ -1269,6 +1298,7 @@ gfx::Rect GetUnclippedParentHypertextRangeBoundsRect( AXClippingBehavior::kClipped) .OffsetFromOrigin(); } +#endif void GetCharacterExtents(AtkText* atk_text, int offset, @@ -2028,6 +2058,7 @@ const gchar* GetName(AtkObject* atk_object) { } const gchar* AtkGetName(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetName); AXPlatformNodeAuraLinux::EnableAXMode(); return GetName(atk_object); } @@ -2045,6 +2076,7 @@ const gchar* GetDescription(AtkObject* atk_object) { } const gchar* AtkGetDescription(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetDescription); AXPlatformNodeAuraLinux::EnableAXMode(); return GetDescription(atk_object); } @@ -2061,6 +2093,7 @@ gint GetNChildren(AtkObject* atk_object) { } gint AtkGetNChildren(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetNChildren); AXPlatformNodeAuraLinux::EnableAXMode(); return GetNChildren(atk_object); } @@ -2083,6 +2116,7 @@ AtkObject* RefChild(AtkObject* atk_object, gint index) { } AtkObject* AtkRefChild(AtkObject* atk_object, gint index) { + RecordAccessibilityAtkApi(UmaAtkApi::kRefChild); AXPlatformNodeAuraLinux::EnableAXMode(); return RefChild(atk_object, index); } @@ -2099,6 +2133,7 @@ gint GetIndexInParent(AtkObject* atk_object) { } gint AtkGetIndexInParent(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetIndexInParent); AXPlatformNodeAuraLinux::EnableAXMode(); return GetIndexInParent(atk_object); } @@ -2115,6 +2150,7 @@ AtkObject* GetParent(AtkObject* atk_object) { } AtkObject* AtkGetParent(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetParent); AXPlatformNodeAuraLinux::EnableAXMode(); return GetParent(atk_object); } @@ -2130,6 +2166,7 @@ AtkRelationSet* RefRelationSet(AtkObject* atk_object) { } AtkRelationSet* AtkRefRelationSet(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kRefRelationSet); AXPlatformNodeAuraLinux::EnableAXMode(); return RefRelationSet(atk_object); } @@ -2146,6 +2183,7 @@ AtkAttributeSet* GetAttributes(AtkObject* atk_object) { } AtkAttributeSet* AtkGetAttributes(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetAttributes); AXPlatformNodeAuraLinux::EnableAXMode(); return GetAttributes(atk_object); } @@ -2161,6 +2199,7 @@ AtkRole GetRole(AtkObject* atk_object) { } AtkRole AtkGetRole(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetRole); AXPlatformNodeAuraLinux::EnableAXMode(); return GetRole(atk_object); } @@ -2183,6 +2222,7 @@ AtkStateSet* RefStateSet(AtkObject* atk_object) { } AtkStateSet* AtkRefStateSet(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kRefStateSet); AXPlatformNodeAuraLinux::EnableAXMode(); return RefStateSet(atk_object); } @@ -2468,6 +2508,8 @@ AtkObject* AXPlatformNodeAuraLinux::CreateAtkObject() { if (GetData().role != ax::mojom::Role::kApplication && !GetAccessibilityMode().has_mode(AXMode::kNativeAPIs)) return nullptr; + if (GetDelegate()->IsChildOfLeaf()) + return nullptr; EnsureGTypeInit(); interface_mask_ = GetGTypeInterfaceMask(GetData()); GType type = GetAccessibilityGType(); @@ -3000,10 +3042,8 @@ void AXPlatformNodeAuraLinux::GetAtkState(AtkStateSet* atk_state_set) { static_cast<int32_t>(ax::mojom::InvalidState::kFalse)) atk_state_set_add_state(atk_state_set, ATK_STATE_INVALID_ENTRY); #if defined(ATK_216) - if (data.HasIntAttribute(ax::mojom::IntAttribute::kCheckedState) && - data.role != ax::mojom::Role::kToggleButton) { + if (IsPlatformCheckable()) atk_state_set_add_state(atk_state_set, ATK_STATE_CHECKABLE); - } if (data.HasIntAttribute(ax::mojom::IntAttribute::kHasPopup)) atk_state_set_add_state(atk_state_set, ATK_STATE_HAS_POPUP); #endif @@ -3217,6 +3257,13 @@ void AXPlatformNodeAuraLinux::Init(AXPlatformNodeDelegate* delegate) { GetOrCreateAtkObject(); } +bool AXPlatformNodeAuraLinux::IsPlatformCheckable() const { + if (GetData().role == ax::mojom::Role::kToggleButton) + return false; + + return AXPlatformNodeBase::IsPlatformCheckable(); +} + void AXPlatformNodeAuraLinux::EnsureAtkObjectIsValid() { if (atk_object_) { // If the object's role changes and that causes its @@ -3343,7 +3390,7 @@ void AXPlatformNodeAuraLinux::OnMenuPopupStart() { atk_object_notify_state_change(parent_frame, ATK_STATE_ACTIVE, TRUE); } -void AXPlatformNodeAuraLinux::OnMenuPopupHide() { +void AXPlatformNodeAuraLinux::OnMenuPopupEnd() { AtkObject* atk_object = GetOrCreateAtkObject(); AtkObject* parent_frame = FindAtkObjectParentFrame(atk_object); if (!parent_frame) @@ -3354,35 +3401,24 @@ void AXPlatformNodeAuraLinux::OnMenuPopupHide() { // kMenuPopupHide may be called multiple times for the same menu, so only // remove it if our parent frame matches the most recently opened menu. std::vector<AtkObject*>& active_menus = GetActiveMenus(); - if (active_menus.empty()) - return; - - // When multiple levels of menu are closed at once, they may be hidden out - // of order. When this happens, we just remove the open menu from the stack. - if (active_menus.back() != atk_object) { - auto it = std::find(active_menus.rbegin(), active_menus.rend(), atk_object); - if (it != active_menus.rend()) { - // We used a reverse iterator, so we need to convert it into a normal - // iterator to use it for std::vector::erase(...). - auto to_remove = --(it.base()); - active_menus.erase(to_remove); - } - return; - } + DCHECK(!active_menus.empty()) + << "Asymmetrical menupopupend events -- too many"; active_menus.pop_back(); - - // We exit early if the newly activated menu has the same AtkWindow as the - // previous one. AtkObject* new_active_item = ComputeActiveTopLevelFrame(); - if (new_active_item == parent_frame) - return; - g_signal_emit_by_name(parent_frame, "deactivate"); - atk_object_notify_state_change(parent_frame, ATK_STATE_ACTIVE, FALSE); - if (new_active_item) { - g_signal_emit_by_name(new_active_item, "activate"); - atk_object_notify_state_change(new_active_item, ATK_STATE_ACTIVE, TRUE); + if (new_active_item != parent_frame) { + // Newly activated menu has the different AtkWindow as the previous one. + g_signal_emit_by_name(parent_frame, "deactivate"); + atk_object_notify_state_change(parent_frame, ATK_STATE_ACTIVE, FALSE); + if (new_active_item) { + g_signal_emit_by_name(new_active_item, "activate"); + atk_object_notify_state_change(new_active_item, ATK_STATE_ACTIVE, TRUE); + } } + + // All menus are closed. + if (active_menus.empty()) + OnAllMenusEnded(); } void AXPlatformNodeAuraLinux::ResendFocusSignalsForCurrentlyFocusedNode() { @@ -3398,7 +3434,8 @@ void AXPlatformNodeAuraLinux::ResendFocusSignalsForCurrentlyFocusedNode() { atk_object_notify_state_change(focused_node, ATK_STATE_FOCUSED, true); } -void AXPlatformNodeAuraLinux::OnMenuPopupEnd() { +// All menus have closed. +void AXPlatformNodeAuraLinux::OnAllMenusEnded() { if (!GetActiveMenus().empty() && g_active_top_level_frame && ComputeActiveTopLevelFrame() != g_active_top_level_frame) { g_signal_emit_by_name(g_active_top_level_frame, "activate"); @@ -3406,8 +3443,8 @@ void AXPlatformNodeAuraLinux::OnMenuPopupEnd() { TRUE); } - ResendFocusSignalsForCurrentlyFocusedNode(); GetActiveMenus().clear(); + ResendFocusSignalsForCurrentlyFocusedNode(); } void AXPlatformNodeAuraLinux::OnWindowActivated() { @@ -3512,16 +3549,25 @@ void AXPlatformNodeAuraLinux::OnFocused() { SetActiveViewsDialog(); - if (g_current_focused) { - g_signal_emit_by_name(g_current_focused, "focus-event", false); - atk_object_notify_state_change(ATK_OBJECT(g_current_focused), + AtkObject* old_effective_focus = g_current_active_descendant + ? g_current_active_descendant + : g_current_focused; + if (old_effective_focus) { + g_signal_emit_by_name(old_effective_focus, "focus-event", false); + atk_object_notify_state_change(ATK_OBJECT(old_effective_focus), ATK_STATE_FOCUSED, false); } SetWeakGPtrToAtkObject(&g_current_focused, atk_object); - g_signal_emit_by_name(atk_object, "focus-event", true); - atk_object_notify_state_change(ATK_OBJECT(atk_object), ATK_STATE_FOCUSED, - true); + AtkObject* descendant = GetActiveDescendantOfCurrentFocused(); + SetWeakGPtrToAtkObject(&g_current_active_descendant, descendant); + + AtkObject* new_effective_focus = g_current_active_descendant + ? g_current_active_descendant + : g_current_focused; + g_signal_emit_by_name(new_effective_focus, "focus-event", true); + atk_object_notify_state_change(ATK_OBJECT(new_effective_focus), + ATK_STATE_FOCUSED, true); } void AXPlatformNodeAuraLinux::OnSelected() { @@ -3874,21 +3920,15 @@ void AXPlatformNodeAuraLinux::NotifyAccessibilityEvent( return; AXPlatformNodeBase::NotifyAccessibilityEvent(event_type); switch (event_type) { - // There are three types of messages that we receive for popup menus. Each - // time a popup menu is shown, we get a kMenuPopupStart message. This - // includes if the menu is hidden and then re-shown. When a menu is hidden - // we receive the kMenuPopupHide message. Finally, when the entire menu is - // closed we receive the kMenuPopupEnd message for the parent menu and all - // of the submenus that were opened when navigating through the menu. - case ax::mojom::Event::kMenuPopupEnd: - OnMenuPopupEnd(); - break; - case ax::mojom::Event::kMenuPopupHide: - OnMenuPopupHide(); - break; + // kMenuStart/kMenuEnd: the menu system has started / stopped. + // kMenuPopupStart/kMenuPopupEnd: an individual menu/submenu has + // opened/closed. case ax::mojom::Event::kMenuPopupStart: OnMenuPopupStart(); break; + case ax::mojom::Event::kMenuPopupEnd: + OnMenuPopupEnd(); + break; case ax::mojom::Event::kActiveDescendantChanged: OnActiveDescendantChanged(); break; @@ -4176,7 +4216,30 @@ gfx::NativeViewAccessible AXPlatformNodeAuraLinux::HitTestSync(gint x, gint y, AtkCoordType coord_type) { gfx::Point scroll_to(x, y); scroll_to = ConvertPointToScreenCoordinates(scroll_to, coord_type); - return delegate_->HitTestSync(scroll_to.x(), scroll_to.y()); + + AXPlatformNode* current_result = this; + while (true) { + gfx::NativeViewAccessible hit_child = + current_result->GetDelegate()->HitTestSync(scroll_to.x(), + scroll_to.y()); + if (!hit_child) + return nullptr; + AXPlatformNode* hit_child_node = + AXPlatformNode::FromNativeViewAccessible(hit_child); + if (!hit_child_node || !hit_child_node->IsDescendantOf(current_result)) + break; + + // If we get the same node, we're done. + if (hit_child_node == current_result) + break; + + // Continue to check recursively. That's because HitTestSync may have + // returned the best result within a particular accessibility tree, + // but we might need to recurse further in a tree of a different type + // (for example, from Views to Web). + current_result = hit_child_node; + } + return current_result->GetNativeViewAccessible(); } bool AXPlatformNodeAuraLinux::GrabFocus() { @@ -4303,9 +4366,9 @@ AtkAttributeSet* AXPlatformNodeAuraLinux::GetAtkAttributes() { AtkStateType AXPlatformNodeAuraLinux::GetAtkStateTypeForCheckableNode() { if (GetData().GetCheckedState() == ax::mojom::CheckedState::kMixed) return ATK_STATE_INDETERMINATE; - if (GetData().role == ax::mojom::Role::kToggleButton) - return ATK_STATE_PRESSED; - return ATK_STATE_CHECKED; + if (IsPlatformCheckable()) + return ATK_STATE_CHECKED; + return ATK_STATE_PRESSED; } // AtkDocumentHelpers diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h index 6fde338d9e1..610b2041b97 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h @@ -206,8 +206,8 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { void OnWindowActivated(); void OnWindowDeactivated(); void OnMenuPopupStart(); - void OnMenuPopupHide(); void OnMenuPopupEnd(); + void OnAllMenusEnded(); void OnSelected(); void OnSelectedChildrenChanged(); void OnTextSelectionChanged(); @@ -237,6 +237,7 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { // AXPlatformNodeBase overrides. void Init(AXPlatformNodeDelegate* delegate) override; + bool IsPlatformCheckable() const override; bool IsNameExposed(); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc index b120f746734..706471c0a84 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc @@ -201,6 +201,7 @@ static bool AtkObjectHasState(AtkObject* atk_object, AtkStateType state) { TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectDetachedObject) { AXNodeData root; root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; root.SetName("Name"); Init(root); @@ -231,6 +232,7 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectDetachedObject) { TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectName) { AXNodeData root; root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; root.SetName("Name"); Init(root); @@ -659,12 +661,14 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkComponentRefAtPoint) { AXNodeData node1; node1.id = 2; + node1.role = ax::mojom::Role::kGenericContainer; node1.relative_bounds.bounds = gfx::RectF(0, 0, 10, 10); node1.SetName("Name1"); root.child_ids.push_back(node1.id); AXNodeData node2; node2.id = 3; + node2.role = ax::mojom::Role::kGenericContainer; node2.relative_bounds.bounds = gfx::RectF(20, 20, 10, 10); node2.SetName("Name2"); root.child_ids.push_back(node2.id); @@ -1820,23 +1824,10 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkPopupWindowActive) { { ActivationTester tester(menu_atk_node); GetPlatformNode(menu_node)->NotifyAccessibilityEvent( - ax::mojom::Event::kMenuPopupHide); - EXPECT_FALSE(tester.saw_activate_); - EXPECT_TRUE(tester.saw_deactivate_); - EXPECT_FALSE(tester.IsActivatedInStateSet()); - EXPECT_EQ(focus_events_on_original_node, 0); - } - - { - ActivationTester tester(menu_atk_node); - GetPlatformNode(menu_node)->NotifyAccessibilityEvent( ax::mojom::Event::kMenuPopupEnd); EXPECT_FALSE(tester.saw_activate_); - EXPECT_FALSE(tester.saw_deactivate_); + EXPECT_TRUE(tester.saw_deactivate_); EXPECT_FALSE(tester.IsActivatedInStateSet()); - - // The menu has closed so the original node should have received focus - // again. EXPECT_EQ(focus_events_on_original_node, 1); } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base.cc b/chromium/ui/accessibility/platform/ax_platform_node_base.cc index 99de6603989..36081b70c14 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_base.cc @@ -4,6 +4,9 @@ #include "ui/accessibility/platform/ax_platform_node_base.h" +#include <algorithm> +#include <limits> +#include <set> #include <string> #include <unordered_map> #include <utility> @@ -26,6 +29,7 @@ namespace ui { namespace { + // A function to call when focus changes, for testing only. base::LazyInstance<std::map<ax::mojom::Event, base::RepeatingClosure>>:: DestructorAtExit g_on_notify_event_for_testing; @@ -54,6 +58,7 @@ bool FindDescendantRoleWithMaxDepth(AXPlatformNodeBase* node, return false; } + } // namespace const base::char16 AXPlatformNodeBase::kEmbeddedCharacter = L'\xfffc'; @@ -139,7 +144,7 @@ gfx::NativeViewAccessible AXPlatformNodeBase::ChildAtIndex(int index) const { std::string AXPlatformNodeBase::GetName() const { if (delegate_) return delegate_->GetName(); - return base::EmptyString(); + return std::string(); } base::string16 AXPlatformNodeBase::GetNameAsString16() const { @@ -495,11 +500,20 @@ bool AXPlatformNodeBase::IsDocument() const { } bool AXPlatformNodeBase::IsTextOnlyObject() const { + if (!delegate_) + return false; + + // In Legacy Layout, a list marker has no children and is thus represented on + // all platforms as a leaf node that exposes the marker itself, i.e., it forms + // part of the AX tree's text representation. In contrast, in Layout NG, a + // list marker has a static text child. + if (GetData().role == ax::mojom::Role::kListMarker) + return !GetChildCount(); return ui::IsText(GetData().role); } bool AXPlatformNodeBase::IsTextField() const { - return IsPlainTextField() || IsRichTextField(); + return GetData().IsTextField(); } bool AXPlatformNodeBase::IsPlainTextField() const { @@ -507,16 +521,18 @@ bool AXPlatformNodeBase::IsPlainTextField() const { } bool AXPlatformNodeBase::IsRichTextField() const { - return GetBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot) && - GetData().HasState(ax::mojom::State::kRichlyEditable); + return GetData().IsRichTextField(); } base::string16 AXPlatformNodeBase::GetHypertext() const { + if (!delegate_) + return base::string16(); + // Hypertext of platform leaves, which internally are composite objects, are // represented with the inner text of the internal composite object. These // don't exist on non-web content. if (IsChildOfLeaf()) - return GetDelegate()->GetInnerText(); + return GetInnerText(); if (hypertext_.needs_update) UpdateComputedHypertext(); @@ -524,38 +540,9 @@ base::string16 AXPlatformNodeBase::GetHypertext() const { } base::string16 AXPlatformNodeBase::GetInnerText() const { - // In order to get the inner text for web content, we potentially need access - // to nodes that are not exposed to platform APIs, i.e. they are only visible - // in the internal accessibility tree. For example, nodes representing the - // shadow DOM inside a native text field. - if (GetDelegate()->IsWebContent()) - return GetDelegate()->GetInnerText(); - - // Allows us to get text even in non-web content, e.g. in the browser's UI - // (AKA Views). - // - // Unlike in web content The "kValue" attribute takes precedence, because the - // accessibility of Views controls are carefully crafted by hand, in contrast - // to HTML pages, where any content that might be present in the shadow DOM - // (i.e. in the internal accessibility tree) is actually used by the renderer. - base::string16 value = - GetString16Attribute(ax::mojom::StringAttribute::kValue); - if (!value.empty()) - return value; - - // TODO(https://crbug.com/1030703): The check for IsInvisibleOrIgnored() - // should not be needed. ChildAtIndex() and GetChildCount() are already - // supposed to skip over nodes that are invisible or ignored, but - // ViewAXPlatformNodeDelegate does not currently implement this behavior. - if (!GetChildCount() && !IsInvisibleOrIgnored()) - return GetNameAsString16(); - - base::string16 text; - for (auto child_iter = AXPlatformNodeChildrenBegin(); - child_iter != AXPlatformNodeChildrenEnd(); ++child_iter) { - text += child_iter->GetInnerText(); - } - return text; + if (!delegate_) + return base::string16(); + return delegate_->GetInnerText(); } bool AXPlatformNodeBase::IsSelectionItemSupported() const { @@ -847,48 +834,15 @@ bool AXPlatformNodeBase::HasCaret( } bool AXPlatformNodeBase::IsLeaf() const { - if (!GetChildCount()) - return true; - - // These types of objects may have children that we use as internal - // implementation details, but we want to expose them as leaves to platform - // accessibility APIs because screen readers might be confused if they find - // any children. - if (IsPlainTextField() || IsTextOnlyObject()) - return true; - - // Roles whose children are only presentational according to the ARIA and - // HTML5 Specs should be hidden from screen readers. - // (Note that whilst ARIA buttons can have only presentational children, HTML5 - // buttons are allowed to have content.) - switch (GetData().role) { - case ax::mojom::Role::kImage: - case ax::mojom::Role::kMeter: - case ax::mojom::Role::kScrollBar: - case ax::mojom::Role::kSlider: - case ax::mojom::Role::kSplitter: - case ax::mojom::Role::kProgressIndicator: - return true; - default: - return false; - } + return delegate_ && delegate_->IsLeaf(); } bool AXPlatformNodeBase::IsChildOfLeaf() const { - AXPlatformNodeBase* ancestor = FromNativeViewAccessible(GetParent()); - - while (ancestor) { - if (ancestor->IsLeaf()) - return true; - ancestor = FromNativeViewAccessible(ancestor->GetParent()); - } - - return false; + return delegate_ && delegate_->IsChildOfLeaf(); } bool AXPlatformNodeBase::IsInvisibleOrIgnored() const { - const AXNodeData& data = GetData(); - return data.HasState(ax::mojom::State::kInvisible) || data.IsIgnored(); + return GetData().IsInvisibleOrIgnored(); } bool AXPlatformNodeBase::IsScrollable() const { @@ -981,7 +935,7 @@ void AXPlatformNodeBase::ComputeAttributes(PlatformAttributeList* attributes) { AddAttributeToList(ax::mojom::IntAttribute::kPosInSet, "posinset", attributes); - if (HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) + if (IsPlatformCheckable()) AddAttributeToList("checkable", "true", attributes); if (IsInvisibleOrIgnored()) // Note: NVDA prefers this over INVISIBLE state. @@ -1280,6 +1234,8 @@ AXHypertext::AXHypertext(const AXHypertext& other) = default; AXHypertext& AXHypertext::operator=(const AXHypertext& other) = default; void AXPlatformNodeBase::UpdateComputedHypertext() const { + if (!delegate_) + return; hypertext_ = AXHypertext(); if (IsLeaf()) { @@ -1728,6 +1684,10 @@ bool AXPlatformNodeBase::IsText(const base::string16& text, return ch != kEmbeddedCharacter; } +bool AXPlatformNodeBase::IsPlatformCheckable() const { + return delegate_ && GetData().HasCheckedState(); +} + void AXPlatformNodeBase::ComputeHypertextRemovedAndInserted( const AXHypertext& old_hypertext, size_t* start, diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base.h b/chromium/ui/accessibility/platform/ax_platform_node_base.h index 90fca62bb3f..c14c8c7e698 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_base.h @@ -216,19 +216,13 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // Optionally accepts an unignored selection to avoid redundant computation. bool HasCaret(const AXTree::Selection* unignored_selection = nullptr); - // Returns true if an ancestor of this node (not including itself) is a - // leaf node, meaning that this node is not actually exposed to the - // platform. + // See AXPlatformNodeDelegate::IsChildOfLeaf(). bool IsChildOfLeaf() const; - // Returns true if this is a leaf node on this platform, meaning any - // children should not be exposed to this platform's native accessibility - // layer. Each platform subclass should implement this itself. - // The definition of a leaf may vary depending on the platform, - // but a leaf node should never have children that are focusable or - // that might send notifications. + // See AXPlatformNodeDelegate::IsLeaf(). bool IsLeaf() const; + // See AXPlatformNodeDelegate::IsInvisibleOrIgnored(). bool IsInvisibleOrIgnored() const; // Returns true if this node can be scrolled either in the horizontal or the @@ -241,34 +235,41 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // Returns true if this node can be scrolled in the vertical direction. bool IsVerticallyScrollable() const; - // Returns true if this node has role of StaticText, LineBreak, or + // Returns true if this node has a role of StaticText, LineBreak, or // InlineTextBox bool IsTextOnlyObject() const; - // A text field is any widget in which the user should be able to enter and - // edit text. - // - // Examples include <input type="text">, <input type="password">, <textarea>, - // <div contenteditable="true">, <div role="textbox">, <div role="searchbox"> - // and <div role="combobox">. Note that when an ARIA role that indicates that - // the widget is editable is used, such as "role=textbox", the element doesn't - // need to be contenteditable for this method to return true, as in theory - // JavaScript could be used to implement editing functionality. In practice, - // this situation should be rare. + // See AXNodeData::IsTextField(). bool IsTextField() const; - // Returns true if the node is an editable text field. + // See AXNodeData::IsPlainTextField(). bool IsPlainTextField() const; + // See AXNodeData::IsRichTextField(). + bool IsRichTextField() const; + + // Determines whether an element should be exposed with checkable state, and + // possibly the checked state. Examples are check box and radio button. + // Objects that are exposed as toggle buttons use the platform pressed state + // in some platform APIs, and should not be exposed as checkable. They don't + // expose the platform equivalent of the internal checked state. + virtual bool IsPlatformCheckable() const; + bool HasFocus(); - // If this node is a leaf, returns the text of this node, otherwise represents - // each child node with a special "embedded object" character. This is how - // text is represented in ATK and IA2 APIs. + // If this node is a leaf, returns the visible accessible name of this node. + // Otherwise represents every non-leaf child node with a special "embedded + // object character", and every leaf child node with its visible accessible + // name. This is how displayed text and embedded objects are represented in + // ATK and IA2 APIs. base::string16 GetHypertext() const; // Returns the text of this node and all descendant nodes; including text // found in embedded objects. + // + // Only text displayed on screen is included. Text from ARIA and HTML + // attributes that is either not displayed on screen, or outside this node, + // e.g. aria-label and HTML title, is not returned. base::string16 GetInnerText() const; virtual base::string16 GetValue() const; @@ -344,11 +345,10 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // // Delegate. This is a weak reference which owns |this|. // - AXPlatformNodeDelegate* delegate_; + AXPlatformNodeDelegate* delegate_ = nullptr; protected: bool IsDocument() const; - bool IsRichTextField() const; bool IsSelectionItemSupported() const; // Get the range value text, which might come from aria-valuetext or @@ -491,7 +491,7 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { mutable AXHypertext hypertext_; private: - // Return true if the index represents a text character. + // Returns true if the index represents a text character. bool IsText(const base::string16& text, size_t index, bool is_indexed_from_end = false); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate.h b/chromium/ui/accessibility/platform/ax_platform_node_delegate.h index c6064c6e248..e8ff6876672 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate.h @@ -78,6 +78,14 @@ class AX_EXPORT AXPlatformNodeDelegate { // Get the accessibility tree data for this node. virtual const AXTreeData& GetTreeData() const = 0; + // Returns the text of this node and all descendant nodes; including text + // found in embedded objects. + // + // Only text displayed on screen is included. Text from ARIA and HTML + // attributes that is either not displayed on screen, or outside this node, + // e.g. aria-label and HTML title, is not returned. + virtual base::string16 GetInnerText() const = 0; + // Get the unignored selection from the tree virtual const AXTree::Selection GetUnignoredSelection() const = 0; @@ -121,12 +129,22 @@ class AX_EXPORT AXPlatformNodeDelegate { virtual gfx::NativeViewAccessible GetPreviousSibling() = 0; // Returns true if an ancestor of this node (not including itself) is a - // leaf node, meaning that this node is not actually exposed to the - // platform. + // leaf node, meaning that this node is not actually exposed to any + // platform's accessibility layer. virtual bool IsChildOfLeaf() const = 0; - // If this object is exposed to the platform, returns this object. Otherwise, - // returns the platform leaf under which this object is found. + // Returns true if this current node is editable and the root editable node is + // a plain text field. + virtual bool IsChildOfPlainTextField() const = 0; + + // Returns true if this is a leaf node, meaning all its + // children should not be exposed to any platform's native accessibility + // layer. + virtual bool IsLeaf() const = 0; + + // If this object is exposed to the platform's accessibility layer, returns + // this object. Otherwise, returns the platform leaf under which this object + // is found. virtual gfx::NativeViewAccessible GetClosestPlatformObject() const = 0; class ChildIterator { @@ -172,10 +190,6 @@ class AX_EXPORT AXPlatformNodeDelegate { // implementations. virtual std::string GetInheritedFontFamilyName() const = 0; - // Returns the text of this node and all descendant nodes; including text - // found in embedded objects. - virtual base::string16 GetInnerText() const = 0; - // Return the bounds of this node in the coordinate system indicated. If the // clipping behavior is set to clipped, clipping is applied. If an offscreen // result address is provided, it will be populated depending on whether the diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc index 8abc5a9300e..39d55498dbd 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc @@ -31,6 +31,38 @@ const AXTreeData& AXPlatformNodeDelegateBase::GetTreeData() const { return *empty_data; } +base::string16 AXPlatformNodeDelegateBase::GetInnerText() const { + // Unlike in web content The "kValue" attribute always takes precedence, + // because we assume that users of this base class, such as Views controls, + // are carefully crafted by hand, in contrast to HTML pages, where any content + // that might be present in the shadow DOM (AKA in the internal accessibility + // tree) is actually used by the renderer when assigning the "kValue" + // attribute, including any redundant white space. + base::string16 value = + GetData().GetString16Attribute(ax::mojom::StringAttribute::kValue); + if (!value.empty()) + return value; + + // TODO(https://crbug.com/1030703): The check for IsInvisibleOrIgnored() + // should not be needed. ChildAtIndex() and GetChildCount() are already + // supposed to skip over nodes that are invisible or ignored, but + // ViewAXPlatformNodeDelegate does not currently implement this behavior. + if (IsLeaf() && !GetData().IsInvisibleOrIgnored()) + return GetData().GetString16Attribute(ax::mojom::StringAttribute::kName); + + base::string16 inner_text; + for (int i = 0; i < GetChildCount(); ++i) { + // TODO(nektar): Add const to all tree traversal methods and remove + // const_cast. + const AXPlatformNode* child = AXPlatformNode::FromNativeViewAccessible( + const_cast<AXPlatformNodeDelegateBase*>(this)->ChildAtIndex(i)); + if (!child || !child->GetDelegate()) + continue; + inner_text += child->GetDelegate()->GetInnerText(); + } + return inner_text; +} + const AXTree::Selection AXPlatformNodeDelegateBase::GetUnignoredSelection() const { return AXTree::Selection{-1, -1, -1, ax::mojom::TextAffinity::kDownstream}; @@ -95,6 +127,21 @@ gfx::NativeViewAccessible AXPlatformNodeDelegateBase::GetPreviousSibling() { } bool AXPlatformNodeDelegateBase::IsChildOfLeaf() const { + // TODO(nektar): Make all tree traversal methods const and remove const_cast. + const AXPlatformNodeDelegate* parent = + const_cast<AXPlatformNodeDelegateBase*>(this)->GetParentDelegate(); + if (!parent) + return false; + if (parent->IsLeaf()) + return true; + return parent->IsChildOfLeaf(); +} + +bool AXPlatformNodeDelegateBase::IsLeaf() const { + return !GetChildCount(); +} + +bool AXPlatformNodeDelegateBase::IsChildOfPlainTextField() const { return false; } @@ -200,10 +247,6 @@ bool AXPlatformNodeDelegateBase::SetHypertextSelection(int start_offset, return AccessibilityPerformAction(action_data); } -base::string16 AXPlatformNodeDelegateBase::GetInnerText() const { - return base::string16(); -} - gfx::Rect AXPlatformNodeDelegateBase::GetBoundsRect( const AXCoordinateSystem coordinate_system, const AXClippingBehavior clipping_behavior, @@ -533,6 +576,11 @@ AXPlatformNodeDelegateBase::GetTargetNodesForRelation( std::set<AXPlatformNode*> AXPlatformNodeDelegateBase::GetReverseRelations( ax::mojom::IntAttribute attr) { + // TODO(accessibility) Implement these if views ever use relations more + // widely. The use so far has been for the Omnibox to the suggestion popup. + // If this is ever implemented, then the "popup for" to "controlled by" + // mapping in AXPlatformRelationWin can be removed, as it would be + // redundant with setting the controls relationship. return std::set<AXPlatformNode*>(); } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h index 350b9f6a44b..7dd68b928d7 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h @@ -35,7 +35,7 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { // Get the accessibility tree data for this node. const AXTreeData& GetTreeData() const override; - // Get the unignored selection from the tree + base::string16 GetInnerText() const override; const AXTree::Selection GetUnignoredSelection() const override; // Creates a text position rooted at this object. @@ -67,13 +67,9 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { gfx::NativeViewAccessible GetNextSibling() override; gfx::NativeViewAccessible GetPreviousSibling() override; - // Returns true if an ancestor of this node (not including itself) is a - // leaf node, meaning that this node is not actually exposed to the - // platform. bool IsChildOfLeaf() const override; - - // If this object is exposed to the platform, returns this object. Otherwise, - // returns the platform leaf under which this object is found. + bool IsChildOfPlainTextField() const override; + bool IsLeaf() const override; gfx::NativeViewAccessible GetClosestPlatformObject() const override; class ChildIteratorBase : public ChildIterator { @@ -107,8 +103,6 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { const TextAttributeList& default_attributes) const override; std::string GetInheritedFontFamilyName() const override; - base::string16 GetInnerText() const override; - gfx::Rect GetBoundsRect(const AXCoordinateSystem coordinate_system, const AXClippingBehavior clipping_behavior, AXOffscreenResult* offscreen_result) const override; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_mac.h b/chromium/ui/accessibility/platform/ax_platform_node_mac.h index 920f0a05363..c28d1a9fdc5 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_mac.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_mac.h @@ -27,6 +27,7 @@ class AXPlatformNodeMac : public AXPlatformNodeBase { // AXPlatformNodeBase. void Destroy() override; + bool IsPlatformCheckable() const override; protected: void AddAttributeToList(const char* name, diff --git a/chromium/ui/accessibility/platform/ax_platform_node_mac.mm b/chromium/ui/accessibility/platform/ax_platform_node_mac.mm index 9a8f6f6d60a..911344e83a4 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_mac.mm +++ b/chromium/ui/accessibility/platform/ax_platform_node_mac.mm @@ -497,6 +497,8 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { return nil; for (id child in [[self AXChildren] reverseObjectEnumerator]) { + if (!NSPointInRect(point, [child accessibilityFrame])) + continue; if (id foundChild = [child accessibilityHitTest:point]) return foundChild; } @@ -744,7 +746,7 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { if (ui::IsNameExposedInAXValueForRole(role)) return [self getName]; - if (_node->HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) { + if (_node->IsPlatformCheckable()) { // Mixed checkbox state not currently supported in views, but could be. // See browser_accessibility_cocoa.mm for details. const auto checkedState = static_cast<ax::mojom::CheckedState>( @@ -844,8 +846,10 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { - (NSValue*)AXSelectedTextRange { // Selection might not be supported. Return (NSRange){0,0} in that case. int start = 0, end = 0; - _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, &start); - _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, &end); + if (_node->IsPlainTextField()) { + start = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart); + end = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd); + } // NSRange cannot represent the direction the text was selected in. return [NSValue valueWithRange:{std::min(start, end), abs(end - start)}]; @@ -1209,6 +1213,17 @@ void AXPlatformNodeMac::Destroy() { AXPlatformNodeBase::Destroy(); } +// On Mac, the checked state is mapped to AXValue. +bool AXPlatformNodeMac::IsPlatformCheckable() const { + if (GetData().role == ax::mojom::Role::kTab) { + // On Mac, tabs are exposed as radio buttons, and are treated as checkable. + // Also, the internal State::kSelected is be mapped to checked via AXValue. + return true; + } + + return AXPlatformNodeBase::IsPlatformCheckable(); +} + gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() { if (!native_node_) native_node_.reset([[AXPlatformNodeCocoa alloc] initWithNode:this]); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc index 167e1af7f8e..ec5adb620d6 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc @@ -60,7 +60,7 @@ class AXPlatformNodeTextChildProviderTest : public AXPlatformNodeWinTest { ui::AXNodeData text_child_of_text; text_child_of_text.id = 6; - text_child_of_text.role = ax::mojom::Role::kStaticText; + text_child_of_text.role = ax::mojom::Role::kInlineTextBox; text_child_of_text.SetName("text child of text."); text_child_of_root.child_ids.push_back(text_child_of_text.id); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textprovider_win_unittest.cc index ec472458f9f..d86ee466120 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textprovider_win_unittest.cc @@ -7,6 +7,8 @@ #include <UIAutomationClient.h> #include <UIAutomationCoreApi.h> +#include <vector> + #include "base/win/scoped_bstr.h" #include "base/win/scoped_safearray.h" #include "ui/accessibility/ax_action_data.h" @@ -107,8 +109,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderRangeFromChild) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); root_data.child_ids.push_back(3); @@ -196,8 +198,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ui::AXNodeData root; root.id = ROOT_ID; - root.SetName("Document"); root.role = ax::mojom::Role::kRootWebArea; + root.SetName("Document"); root.child_ids = {DIALOG_ID}; ui::AXNodeData dialog; @@ -343,8 +345,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderDocumentRange) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); Init(root_data, text_data); @@ -374,8 +376,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderDocumentRangeNested) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); Init(root_data, paragraph_data, text_data); @@ -400,8 +402,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderSupportedSelection) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); Init(root_data, text_data); @@ -433,8 +435,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetSelection) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); root_data.child_ids.push_back(3); @@ -608,8 +610,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetActiveComposition) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); ui::AXTreeUpdate update; @@ -668,8 +670,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetConversionTarget) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); ui::AXTreeUpdate update; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc index ee4026570b1..379fa000578 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc @@ -172,6 +172,11 @@ HRESULT AXPlatformNodeTextRangeProviderWin::CompareEndpoints( HRESULT AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit( TextUnit unit) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_EXPANDTOENCLOSINGUNIT); + return ExpandToEnclosingUnitImpl(unit); +} + +HRESULT AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnitImpl( + TextUnit unit) { UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); NormalizeTextRange(); @@ -627,8 +632,8 @@ HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, // Move the start of the text range forward or backward in the document by the // requested number of text unit boundaries. int start_units_moved = 0; - HRESULT hr = MoveEndpointByUnit(TextPatternRangeEndpoint_Start, unit, count, - &start_units_moved); + HRESULT hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_Start, unit, + count, &start_units_moved); bool succeeded_move = SUCCEEDED(hr) && start_units_moved != 0; if (succeeded_move) { @@ -640,8 +645,8 @@ HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, // by one text unit to expand the text range from the degenerate range // state. int current_start_units_moved = 0; - hr = MoveEndpointByUnit(TextPatternRangeEndpoint_Start, unit, -1, - ¤t_start_units_moved); + hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_Start, unit, -1, + ¤t_start_units_moved); start_units_moved -= 1; succeeded_move = SUCCEEDED(hr) && current_start_units_moved == -1 && start_units_moved > 0; @@ -650,8 +655,8 @@ HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, // forward by one text unit to expand the text range from the degenerate // state. int end_units_moved = 0; - hr = MoveEndpointByUnit(TextPatternRangeEndpoint_End, unit, 1, - &end_units_moved); + hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_End, unit, 1, + &end_units_moved); succeeded_move = SUCCEEDED(hr) && end_units_moved == 1; } @@ -660,7 +665,7 @@ HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, // sure to bring back the end endpoint to the end of the start's anchor. if (start_->anchor_id() != end_->anchor_id() && (unit == TextUnit_Character || unit == TextUnit_Word)) { - ExpandToEnclosingUnit(unit); + ExpandToEnclosingUnitImpl(unit); } } } @@ -683,6 +688,14 @@ HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit( int count, int* units_moved) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENDPOINTBYUNIT); + return MoveEndpointByUnitImpl(endpoint, unit, count, units_moved); +} + +HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnitImpl( + TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved) { UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(units_moved); // Per MSDN, MoveEndpointByUnit with zero count has no effect. @@ -1124,12 +1137,22 @@ void AXPlatformNodeTextRangeProviderWin::NormalizeTextRange() { NormalizeAsUnignoredTextRange(); // Do not normalize text ranges when a cursor or selection is visible. ATs - // may depend on the specific position that the caret or selection is at. + // may depend on the specific position that the caret or selection is at. This + // condition fixes issues when the caret is inside a plain text field, but + // causes more issues when used inside of a rich text field. For this reason, + // if we have a caret or a selection inside of an editable node, restrict this + // to a plain text field as we gain nothing from using it in a rich text + // field. AXPlatformNodeDelegate* start_delegate = GetDelegate(start_.get()); AXPlatformNodeDelegate* end_delegate = GetDelegate(end_.get()); - if ((start_delegate && start_delegate->HasVisibleCaretOrSelection()) || - (start_delegate && end_delegate->HasVisibleCaretOrSelection())) + if ((start_delegate && start_delegate->HasVisibleCaretOrSelection() && + (!start_delegate->GetData().HasState(ax::mojom::State::kEditable) || + start_delegate->IsChildOfPlainTextField())) || + (end_delegate && end_delegate->HasVisibleCaretOrSelection() && + (!end_delegate->GetData().HasState(ax::mojom::State::kEditable) || + end_delegate->IsChildOfPlainTextField()))) { return; + } AXPositionInstance normalized_start = start_->AsLeafTextPositionBeforeCharacter(); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h index db595fdf424..c967b191124 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h @@ -99,6 +99,16 @@ class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) AXBoundaryBehavior boundary_behavior, ax::mojom::MoveDirection boundary_direction); + // Prefer these *Impl methods when functionality is needed internally. We + // should avoid calling external APIs internally as it will cause the + // histograms to become innaccurate. + HRESULT MoveEndpointByUnitImpl(TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved); + + IFACEMETHODIMP ExpandToEnclosingUnitImpl(TextUnit unit); + base::string16 GetString(int max_count, size_t* appended_newlines_count = nullptr); AXPlatformNodeWin* owner() const; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc index 13617fa8a8c..a698ca3c021 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc @@ -7,6 +7,9 @@ #include <UIAutomationClient.h> #include <UIAutomationCoreApi.h> +#include <memory> +#include <utility> + #include "base/win/atl.h" #include "base/win/scoped_bstr.h" #include "base/win/scoped_safearray.h" @@ -3074,24 +3077,12 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, update.tree_data = tree_data; update.has_tree_data = true; update.root_id = root_data.id; - update.nodes.push_back(root_data); - update.nodes.push_back(paragraph_data); - update.nodes.push_back(static_text_data1); - update.nodes.push_back(inline_text_data1); - update.nodes.push_back(link_data); - update.nodes.push_back(static_text_data2); - update.nodes.push_back(inline_text_data2); - update.nodes.push_back(link_data2); - update.nodes.push_back(list_data); - update.nodes.push_back(list_item_data); - update.nodes.push_back(static_text_data3); - update.nodes.push_back(inline_text_data3); - update.nodes.push_back(search_box); - update.nodes.push_back(search_text); - update.nodes.push_back(pdf_highlight_data); - update.nodes.push_back(static_text_data4); - update.nodes.push_back(inline_text_data4); - + update.nodes = {root_data, paragraph_data, static_text_data1, + inline_text_data1, link_data, static_text_data2, + inline_text_data2, link_data2, list_data, + list_item_data, static_text_data3, inline_text_data3, + search_box, search_text, pdf_highlight_data, + static_text_data4, inline_text_data4}; Init(update); // Set up variables from the tree for testing. diff --git a/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc index 480a7233253..1cc881f5af8 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc @@ -359,31 +359,31 @@ AXTreeUpdate AXPlatformNodeTest::BuildListBox( const std::vector<ax::mojom::State>& additional_state) { AXNodeData listbox; listbox.id = 1; - listbox.SetName("ListBox"); listbox.role = ax::mojom::Role::kListBox; + listbox.SetName("ListBox"); for (auto state : additional_state) listbox.AddState(state); AXNodeData option_1; option_1.id = 2; - option_1.SetName("Option1"); option_1.role = ax::mojom::Role::kListBoxOption; + option_1.SetName("Option1"); if (option_1_is_selected) option_1.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); listbox.child_ids.push_back(option_1.id); AXNodeData option_2; option_2.id = 3; - option_2.SetName("Option2"); option_2.role = ax::mojom::Role::kListBoxOption; + option_2.SetName("Option2"); if (option_2_is_selected) option_2.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); listbox.child_ids.push_back(option_2.id); AXNodeData option_3; option_3.id = 4; - option_3.SetName("Option3"); option_3.role = ax::mojom::Role::kListBoxOption; + option_3.SetName("Option3"); if (option_3_is_selected) option_3.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); listbox.child_ids.push_back(option_3.id); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win.cc b/chromium/ui/accessibility/platform/ax_platform_node_win.cc index b9fbc55ae57..d0fbf20bcf0 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_win.cc @@ -15,11 +15,14 @@ #include <utility> #include <vector> +#include "base/json/json_writer.h" #include "base/lazy_instance.h" #include "base/metrics/histogram_functions.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/values.h" #include "base/win/enum_variant.h" #include "base/win/scoped_bstr.h" #include "base/win/scoped_safearray.h" @@ -27,6 +30,7 @@ #include "skia/ext/skia_utils_win.h" #include "third_party/iaccessible2/ia2_api_all.h" #include "third_party/skia/include/core/SkColor.h" +#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_switches.h" #include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_active_popup.h" @@ -43,6 +47,7 @@ #include "ui/accessibility/platform/ax_platform_node_textchildprovider_win.h" #include "ui/accessibility/platform/ax_platform_node_textprovider_win.h" #include "ui/accessibility/platform/ax_platform_relation_win.h" +#include "ui/accessibility/platform/uia_registrar_win.h" #include "ui/base/win/atl_module.h" #include "ui/display/win/screen_win.h" #include "ui/gfx/geometry/rect_conversions.h" @@ -297,7 +302,7 @@ AXPlatformNode* AXPlatformNode::FromNativeViewAccessible( // AXPlatformNodeWin // -AXPlatformNodeWin::AXPlatformNodeWin() : force_new_hypertext_(false) {} +AXPlatformNodeWin::AXPlatformNodeWin() {} AXPlatformNodeWin::~AXPlatformNodeWin() { ClearOwnRelations(); @@ -305,7 +310,6 @@ AXPlatformNodeWin::~AXPlatformNodeWin() { void AXPlatformNodeWin::Init(AXPlatformNodeDelegate* delegate) { AXPlatformNodeBase::Init(delegate); - force_new_hypertext_ = false; } void AXPlatformNodeWin::ClearOwnRelations() { @@ -314,10 +318,6 @@ void AXPlatformNodeWin::ClearOwnRelations() { relations_.clear(); } -void AXPlatformNodeWin::ForceNewHypertext() { - force_new_hypertext_ = true; -} - // Static void AXPlatformNodeWin::SanitizeStringAttributeForUIAAriaProperty( const base::string16& input, @@ -645,7 +645,7 @@ void AXPlatformNodeWin::NotifyAccessibilityEvent(ax::mojom::Event event_type) { ::VariantInit(old_value.Receive()); base::win::ScopedVariant new_value; ::VariantInit(new_value.Receive()); - GetPropertyValue((*uia_property), new_value.Receive()); + GetPropertyValueImpl((*uia_property), new_value.Receive()); ::UiaRaiseAutomationPropertyChangedEvent(this, (*uia_property), old_value, new_value); } @@ -1263,6 +1263,14 @@ IFACEMETHODIMP AXPlatformNodeWin::get_states(AccessibleStates* states) { IFACEMETHODIMP AXPlatformNodeWin::get_uniqueID(LONG* id) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_UNIQUE_ID); COM_OBJECT_VALIDATE_1_ARG(id); + // We want to negate the unique id for it to be consistent across different + // Windows accessiblity APIs. The negative unique id convention originated + // from ::NotifyWinEvent() takes an hwnd and a child id. A 0 child id means + // self, and a positive child id means child #n. In order to fire an event for + // an arbitrary descendant of the window, Firefox started the practice of + // using a negative unique id. We follow the same negative unique id + // convention here and when we fire events via + // ::NotifyWinEvent(). *id = -GetUniqueId(); return S_OK; } @@ -2075,7 +2083,23 @@ HRESULT AXPlatformNodeWin::ISelectionItemProviderSetSelected( return UIA_E_ELEMENTNOTENABLED; } - if (selected == ISelectionItemProviderIsSelected()) + // The platform implements selection follows focus for single-selection + // container elements. Focus action can change a node's accessibility selected + // state, but does not cause the actual control to be selected. + // https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_selection_follows_focus + // https://www.w3.org/TR/core-aam-1.2/#mapping_events_selection + // + // We don't want to perform |Action::kDoDefault| for an ax node that has + // |kSelected=true| and |kSelectedFromFocus=false|, because perform + // |Action::kDoDefault| may cause the control to be unselected. However, if an + // ax node is selected due to focus, i.e. |kSelectedFromFocus=true|, we need + // to perform |Action::kDoDefault| on the ax node, since focus action only + // changes an ax node's accessibility selected state to |kSelected=true| and + // no |Action::kDoDefault| was performed on that node yet. So we need to + // perform |Action::kDoDefault| on the ax node to cause its associated control + // to be selected. + if (selected == ISelectionItemProviderIsSelected() && + !GetBoolAttribute(ax::mojom::BoolAttribute::kSelectedFromFocus)) return S_OK; AXActionData data; @@ -3971,6 +3995,11 @@ IFACEMETHODIMP AXPlatformNodeWin::get_FragmentRoot( IFACEMETHODIMP AXPlatformNodeWin::GetPatternProvider(PATTERNID pattern_id, IUnknown** result) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PATTERN_PROVIDER); + return GetPatternProviderImpl(pattern_id, result); +} + +HRESULT AXPlatformNodeWin::GetPatternProviderImpl(PATTERNID pattern_id, + IUnknown** result) { UIA_VALIDATE_CALL_1_ARG(result); *result = nullptr; @@ -3997,7 +4026,11 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, // Collapse all unknown property IDs into a single bucket. base::UmaHistogramSparse("Accessibility.WinAPIs.GetPropertyValue", 0); } + return GetPropertyValueImpl(property_id, result); +} +HRESULT AXPlatformNodeWin::GetPropertyValueImpl(PROPERTYID property_id, + VARIANT* result) { UIA_VALIDATE_CALL_1_ARG(result); result->vt = VT_EMPTY; @@ -4005,6 +4038,7 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, int int_attribute; const AXNodeData& data = GetData(); + // Default UIA Property Ids. switch (property_id) { case UIA_AriaPropertiesPropertyId: result->vt = VT_BSTR; @@ -4044,9 +4078,7 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, break; case UIA_CulturePropertyId: - result->vt = VT_BSTR; - GetStringAttributeAsBstr(ax::mojom::StringAttribute::kLanguage, - &result->bstrVal); + return GetCultureAttributeAsVariant(result); break; case UIA_DescribedByPropertyId: @@ -4366,6 +4398,21 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, case UIA_ProviderDescriptionPropertyId: case UIA_RuntimeIdPropertyId: break; + } // End of default UIA property ids. + + // Custom UIA Property Ids. + if (property_id == + UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId()) { + // We want to negate the unique id for it to be consistent across different + // Windows accessiblity APIs. The negative unique id convention originated + // from ::NotifyWinEvent() takes an hwnd and a child id. A 0 child id means + // self, and a positive child id means child #n. In order to fire an event + // for an arbitrary descendant of the window, Firefox started the practice + // of using a negative unique id. We follow the same negative unique id + // convention here and when we fire events via ::NotifyWinEvent(). + result->vt = VT_BSTR; + result->bstrVal = + SysAllocString(base::NumberToString16(-GetUniqueId()).c_str()); } return S_OK; @@ -4405,6 +4452,71 @@ IFACEMETHODIMP AXPlatformNodeWin::ShowContextMenu() { } // +// IChromeAccessible implementation. +// + +void SendBulkFetchResponse( + Microsoft::WRL::ComPtr<IChromeAccessibleDelegate> delegate, + LONG request_id, + std::string json_result) { + base::string16 json_result_utf16 = base::UTF8ToUTF16(json_result); + delegate->put_bulkFetchResult(request_id, + SysAllocString(json_result_utf16.c_str())); +} + +IFACEMETHODIMP AXPlatformNodeWin::get_bulkFetch( + BSTR input_json, + LONG request_id, + IChromeAccessibleDelegate* delegate) { + COM_OBJECT_VALIDATE(); + if (!delegate) + return E_INVALIDARG; + + // TODO(crbug.com/1083834): if parsing |input_json|, use + // DataDecoder because the json is untrusted. For now, this is just + // a stub that calls PostTask so that it's async, but it doesn't + // actually parse the input. + + base::Value result(base::Value::Type::DICTIONARY); + result.SetKey("role", base::Value(ui::ToString(GetData().role))); + + gfx::Rect bounds = GetDelegate()->GetBoundsRect( + AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kUnclipped); + result.SetKey("x", base::Value(bounds.x())); + result.SetKey("y", base::Value(bounds.y())); + result.SetKey("width", base::Value(bounds.width())); + result.SetKey("height", base::Value(bounds.height())); + std::string json_result; + base::JSONWriter::Write(result, &json_result); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce( + &SendBulkFetchResponse, + Microsoft::WRL::ComPtr<IChromeAccessibleDelegate>(delegate), + request_id, json_result)); + return S_OK; +} + +IFACEMETHODIMP AXPlatformNodeWin::get_hitTest( + LONG screen_physical_pixel_x, + LONG screen_physical_pixel_y, + LONG request_id, + IChromeAccessibleDelegate* delegate) { + COM_OBJECT_VALIDATE(); + + if (!delegate) + return E_INVALIDARG; + + // TODO(crbug.com/1083834): Plumb through an actual async hit test. + AXPlatformNodeWin* hit_child = static_cast<AXPlatformNodeWin*>( + FromNativeViewAccessible(GetDelegate()->HitTestSync( + screen_physical_pixel_x, screen_physical_pixel_y))); + + delegate->put_hitTestResult(request_id, static_cast<IAccessible*>(hit_child)); + return S_OK; +} + +// // IServiceProvider implementation. // @@ -4430,6 +4542,12 @@ IFACEMETHODIMP AXPlatformNodeWin::QueryService(REFGUID guidService, return QueryInterface(riid, object); } + if (guidService == IID_IChromeAccessible) { + if (features::IsIChromeAccessibleEnabled()) { + return QueryInterface(riid, object); + } + } + // TODO(suproteem): Include IAccessibleEx in the list, potentially checking // for version. @@ -4468,6 +4586,10 @@ STDMETHODIMP AXPlatformNodeWin::InternalQueryInterface( if (!accessible->GetData().IsRangeValueSupported()) { return E_NOINTERFACE; } + } else if (riid == IID_IChromeAccessible) { + if (!features::IsIChromeAccessibleEnabled()) { + return E_NOINTERFACE; + } } return CComObjectRootBase::InternalQueryInterface(this_ptr, entries, riid, @@ -5254,7 +5376,7 @@ int32_t AXPlatformNodeWin::ComputeIA2State() { const AXNodeData& data = GetData(); int32_t ia2_state = IA2_STATE_OPAQUE; - if (HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) + if (IsPlatformCheckable()) ia2_state |= IA2_STATE_CHECKABLE; if (HasIntAttribute(ax::mojom::IntAttribute::kInvalidState) && @@ -6800,7 +6922,12 @@ bool AXPlatformNodeWin::IsUIAControl() const { // UIA provides multiple "views": raw, content and control. We only want to // populate the content and control views with items that make sense to // traverse over. + if (GetDelegate()->IsWebContent()) { + // Invisible or ignored elements should not show up in control view at all. + if (IsInvisibleOrIgnored()) + return false; + if (IsTextOnlyObject()) { // A text leaf can be a UIAControl, but text inside of a heading, link, // button, etc. where the role allows the name to be generated from the @@ -6840,7 +6967,8 @@ bool AXPlatformNodeWin::IsUIAControl() const { } parent = FromNativeViewAccessible(parent->GetParent()); } - } + } // end of text only case. + const AXNodeData& data = GetData(); // https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-treeoverview#control-view // The control view also includes noninteractive UI items that contribute @@ -6892,9 +7020,10 @@ bool AXPlatformNodeWin::IsUIAControl() const { !data.HasState(ax::mojom::State::kFocusable) && !data.IsClickable()) { return false; } + return true; - } - // non web-content case. + } // end of web-content only case. + const AXNodeData& data = GetData(); return !((IsReadOnlySupported(data.role) && data.IsReadOnlyOrDisabled()) || data.HasState(ax::mojom::State::kInvisible) || @@ -6959,8 +7088,10 @@ bool AXPlatformNodeWin::IsInaccessibleDueToAncestor() const { } bool AXPlatformNodeWin::ShouldHideChildrenForUIA() const { - auto role = GetData().role; + if (IsPlainTextField()) + return true; + auto role = GetData().role; if (HasPresentationalChildren(role)) return true; @@ -6982,7 +7113,6 @@ bool AXPlatformNodeWin::ShouldHideChildrenForUIA() const { return only_child && only_child->IsTextOnlyObject(); } return false; - case ax::mojom::Role::kTextField: case ax::mojom::Role::kPdfActionableHighlight: return true; default: @@ -7004,6 +7134,13 @@ base::string16 AXPlatformNodeWin::GetValue() const { return value; } +bool AXPlatformNodeWin::IsPlatformCheckable() const { + if (GetData().role == ax::mojom::Role::kToggleButton) + return false; + + return AXPlatformNodeBase::IsPlatformCheckable(); +} + bool AXPlatformNodeWin::ShouldNodeHaveFocusableState( const AXNodeData& data) const { switch (data.role) { @@ -7212,6 +7349,8 @@ base::Optional<DWORD> AXPlatformNodeWin::MojoEventToMSAAEvent( switch (event) { case ax::mojom::Event::kAlert: return EVENT_SYSTEM_ALERT; + case ax::mojom::Event::kActiveDescendantChanged: + return IA2_EVENT_ACTIVE_DESCENDANT_CHANGED; case ax::mojom::Event::kCheckedStateChanged: case ax::mojom::Event::kExpandedChanged: case ax::mojom::Event::kStateChanged: diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win.h b/chromium/ui/accessibility/platform/ax_platform_node_win.h index 1055422d59c..9f4367393bc 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_win.h @@ -28,6 +28,7 @@ #include "ui/accessibility/ax_text_utils.h" #include "ui/accessibility/platform/ax_platform_node_base.h" #include "ui/accessibility/platform/ax_platform_text_boundary.h" +#include "ui/accessibility/platform/ichromeaccessible.h" #include "ui/gfx/range/range.h" // IMPORTANT! @@ -280,6 +281,11 @@ enum { UMA_API_WINDOW_GET_WINDOWVISUALSTATE = 243, UMA_API_WINDOW_GET_WINDOWINTERACTIONSTATE = 244, UMA_API_WINDOW_GET_ISTOPMOST = 245, + UMA_API_ELEMENT_PROVIDER_FROM_POINT = 246, + UMA_API_GET_FOCUS = 247, + UMA_API_ADVISE_EVENT_ADDED = 248, + UMA_API_ADVISE_EVENT_REMOVED = 249, + UMA_API_ITEMCONTAINER_FINDITEMBYPROPERTY = 250, // This must always be the last enum. It's okay for its value to // increase, but none of the other enum values may change. @@ -358,6 +364,7 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) public IToggleProvider, public IValueProvider, public IWindowProvider, + public IChromeAccessible, public AXPlatformNodeBase { using IDispatchImpl::Invoke; @@ -381,6 +388,7 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) COM_INTERFACE_ENTRY(IAccessibleTable2) COM_INTERFACE_ENTRY(IAccessibleTableCell) COM_INTERFACE_ENTRY(IAccessibleValue) + COM_INTERFACE_ENTRY(IChromeAccessible) COM_INTERFACE_ENTRY(IExpandCollapseProvider) COM_INTERFACE_ENTRY(IGridItemProvider) COM_INTERFACE_ENTRY(IGridProvider) @@ -408,8 +416,6 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) // Clear any AXPlatformRelationWin nodes owned by this node. void ClearOwnRelations(); - void ForceNewHypertext(); - // AXPlatformNode overrides. gfx::NativeViewAccessible GetNativeViewAccessible() override; void NotifyAccessibilityEvent(ax::mojom::Event event_type) override; @@ -417,6 +423,7 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) // AXPlatformNodeBase overrides. void Destroy() override; base::string16 GetValue() const override; + bool IsPlatformCheckable() const override; // // IAccessible methods. @@ -1023,6 +1030,19 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) IFACEMETHODIMP ShowContextMenu() override; // + // IChromeAccessible methods. + // + + IFACEMETHODIMP get_bulkFetch(BSTR input_json, + LONG request_id, + IChromeAccessibleDelegate* delegate) override; + + IFACEMETHODIMP get_hitTest(LONG screen_physical_pixel_x, + LONG screen_physical_pixel_y, + LONG request_id, + IChromeAccessibleDelegate* delegate) override; + + // // IServiceProvider methods. // @@ -1046,6 +1066,16 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) // IRawElementProviderSimple support method. bool IsPatternProviderSupported(PATTERNID pattern_id); + // Prefer GetPatternProviderImpl when calling internally. We should avoid + // calling external APIs internally as it will cause the histograms to become + // innaccurate. + HRESULT GetPatternProviderImpl(PATTERNID pattern_id, IUnknown** result); + + // Prefer GetPropertyValueImpl when calling internally. We should avoid + // calling external APIs internally as it will cause the histograms to become + // innaccurate. + HRESULT GetPropertyValueImpl(PROPERTYID property_id, VARIANT* result); + // Helper to return the runtime id (without going through a SAFEARRAY) using RuntimeIdArray = std::array<int, 2>; void GetRuntimeIdArray(RuntimeIdArray& runtime_id); @@ -1123,7 +1153,6 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) std::vector<Microsoft::WRL::ComPtr<AXPlatformRelationWin>> relations_; AXHypertext old_hypertext_; - bool force_new_hypertext_; // These protected methods are still used by BrowserAccessibilityComWin. At // some point post conversion, we can probably move these to be private diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc index 50b78af9642..c7fce2de057 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc @@ -11,8 +11,11 @@ #include <memory> #include "base/auto_reset.h" +#include "base/json/json_reader.h" +#include "base/run_loop.h" #include "base/stl_util.h" #include "base/test/metrics/histogram_tester.h" +#include "base/test/task_environment.h" #include "base/win/atl.h" #include "base/win/scoped_bstr.h" #include "base/win/scoped_safearray.h" @@ -20,6 +23,7 @@ #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/iaccessible2/ia2_api_all.h" +#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/platform/ax_fragment_root_win.h" @@ -219,7 +223,10 @@ ScopedVariant SELF(CHILDID_SELF); testing::UnorderedElementsAreArray(expected_property_values)); \ } -AXPlatformNodeWinTest::AXPlatformNodeWinTest() {} +AXPlatformNodeWinTest::AXPlatformNodeWinTest() { + scoped_feature_list_.InitAndEnableFeature(features::kIChromeAccessible); +} + AXPlatformNodeWinTest::~AXPlatformNodeWinTest() {} void AXPlatformNodeWinTest::SetUp() { @@ -451,6 +458,7 @@ bool TestFragmentRootDelegate::IsAXFragmentRootAControlElement() { TEST_F(AXPlatformNodeWinTest, IAccessibleDetachedObject) { AXNodeData root; root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; root.SetName("Name"); Init(root); @@ -472,12 +480,14 @@ TEST_F(AXPlatformNodeWinTest, IAccessibleHitTest) { AXNodeData node1; node1.id = 2; + node1.role = ax::mojom::Role::kGenericContainer; node1.relative_bounds.bounds = gfx::RectF(0, 0, 10, 10); node1.SetName("Name1"); root.child_ids.push_back(node1.id); AXNodeData node2; node2.id = 3; + node2.role = ax::mojom::Role::kGenericContainer; node2.relative_bounds.bounds = gfx::RectF(20, 20, 20, 20); node2.SetName("Name2"); root.child_ids.push_back(node2.id); @@ -512,6 +522,7 @@ TEST_F(AXPlatformNodeWinTest, IAccessibleHitTestDoesNotLoopForever) { AXNodeData node1; node1.id = 2; + node1.role = ax::mojom::Role::kGenericContainer; node1.relative_bounds.bounds = gfx::RectF(0, 0, 10, 10); node1.SetName("Name1"); root.child_ids.push_back(node1.id); @@ -535,6 +546,7 @@ TEST_F(AXPlatformNodeWinTest, IAccessibleHitTestDoesNotLoopForever) { TEST_F(AXPlatformNodeWinTest, IAccessibleName) { AXNodeData root; root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; root.SetName("Name"); Init(root); @@ -1928,6 +1940,103 @@ TEST_F(AXPlatformNodeWinTest, IAccessible2GetNRelations) { // TODO(dougt): Try adding one more relation. } +TEST_F(AXPlatformNodeWinTest, + IAccessible2TestPopupForRelationMapsToControlledByRelation) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + + AXNodeData child1; + child1.id = 2; + child1.role = ax::mojom::Role::kTextField; + child1.AddIntListAttribute(ax::mojom::IntListAttribute::kControlsIds, {3}); + root.child_ids.push_back(2); + + // Add listbox that is popup for the textfield. + AXNodeData child2; + child2.id = 3; + child2.role = ax::mojom::Role::kListBox; + child2.AddIntAttribute(ax::mojom::IntAttribute::kPopupForId, 2); + root.child_ids.push_back(3); + + Init(root, child1, child2); + ComPtr<IAccessible> root_iaccessible(GetRootIAccessible()); + ComPtr<IAccessible2> root_iaccessible2 = ToIAccessible2(root_iaccessible); + + ComPtr<IDispatch> result; + EXPECT_EQ(S_OK, root_iaccessible2->get_accChild(ScopedVariant(1), &result)); + ComPtr<IAccessible2> ax_child1; + EXPECT_EQ(S_OK, result.As(&ax_child1)); + result.Reset(); + + EXPECT_EQ(S_OK, root_iaccessible2->get_accChild(ScopedVariant(2), &result)); + ComPtr<IAccessible2> ax_child2; + EXPECT_EQ(S_OK, result.As(&ax_child2)); + result.Reset(); + + LONG n_relations = 0; + LONG n_targets = 0; + ScopedBstr relation_type; + ComPtr<IAccessibleRelation> controls_relation; + ComPtr<IAccessibleRelation> controlled_by_relation; + ComPtr<IUnknown> target; + + EXPECT_HRESULT_SUCCEEDED(ax_child1->get_nRelations(&n_relations)); + EXPECT_EQ(1, n_relations); + + EXPECT_HRESULT_SUCCEEDED(ax_child1->get_relation(0, &controls_relation)); + + EXPECT_HRESULT_SUCCEEDED( + controls_relation->get_relationType(relation_type.Receive())); + EXPECT_EQ(L"controllerFor", base::string16(relation_type.Get())); + + relation_type.Reset(); + + EXPECT_HRESULT_SUCCEEDED(controls_relation->get_nTargets(&n_targets)); + EXPECT_EQ(1, n_targets); + + EXPECT_HRESULT_SUCCEEDED(controls_relation->get_target(0, &target)); + target.Reset(); + + controls_relation.Reset(); + + // Test the controlled by relation, mapped from the popup for relation. + EXPECT_HRESULT_SUCCEEDED(ax_child2->get_nRelations(&n_relations)); + // The test is currently outsmarting us, and automatically mapping the + // reverse relation in addition to mapping the popup for -> controlled by. + // Therefore, the same relation will exist twice in this test, which + // actually shows that the popup for -> controlled by relation is working. + // As a result, both relations should have the same result in this test. + EXPECT_EQ(2, n_relations); + + // Both relations should have the same result in this test. + EXPECT_HRESULT_SUCCEEDED(ax_child2->get_relation(0, &controlled_by_relation)); + EXPECT_HRESULT_SUCCEEDED( + controlled_by_relation->get_relationType(relation_type.Receive())); + EXPECT_EQ(L"controlledBy", base::string16(relation_type.Get())); + relation_type.Reset(); + + EXPECT_HRESULT_SUCCEEDED(controlled_by_relation->get_nTargets(&n_targets)); + EXPECT_EQ(1, n_targets); + + EXPECT_HRESULT_SUCCEEDED(controlled_by_relation->get_target(0, &target)); + target.Reset(); + controlled_by_relation.Reset(); + + // Both relations should have the same result in this test. + EXPECT_HRESULT_SUCCEEDED(ax_child2->get_relation(1, &controlled_by_relation)); + EXPECT_HRESULT_SUCCEEDED( + controlled_by_relation->get_relationType(relation_type.Receive())); + EXPECT_EQ(L"controlledBy", base::string16(relation_type.Get())); + relation_type.Reset(); + + EXPECT_HRESULT_SUCCEEDED(controlled_by_relation->get_nTargets(&n_targets)); + EXPECT_EQ(1, n_targets); + + EXPECT_HRESULT_SUCCEEDED(controlled_by_relation->get_target(0, &target)); + target.Reset(); +} + TEST_F(AXPlatformNodeWinTest, DISABLED_TestRelationTargetsOfType) { AXNodeData root; root.id = 1; @@ -3474,6 +3583,124 @@ TEST_F(AXPlatformNodeWinTest, ITableProviderGetColumnHeaders) { EXPECT_EQ(nullptr, safearray.Get()); } +TEST_F(AXPlatformNodeWinTest, ITableProviderGetColumnHeadersMultipleHeaders) { + // Build a table like this: + // header_r1c1 | header_r1c2 | header_r1c3 + // cell_r2c1 | cell_r2c2 | cell_r2c3 + // cell_r3c1 | header_r3c2 | + + // <table> + // <tr aria-label="row1"> + // <th>header_r1c1</th> + // <th>header_r1c2</th> + // <th>header_r1c3</th> + // </tr> + // <tr aria-label="row2"> + // <td>cell_r2c1</td> + // <td>cell_r2c2</td> + // <td>cell_r2c3</td> + // </tr> + // <tr aria-label="row3"> + // <td>cell_r3c1</td> + // <th>header_r3c2</th> + // </tr> + // </table> + + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kTable; + + AXNodeData row1; + row1.id = 2; + row1.role = ax::mojom::Role::kRow; + root.child_ids.push_back(row1.id); + + AXNodeData row2; + row2.id = 3; + row2.role = ax::mojom::Role::kRow; + root.child_ids.push_back(row2.id); + + AXNodeData row3; + row3.id = 4; + row3.role = ax::mojom::Role::kRow; + root.child_ids.push_back(row3.id); + + // <tr aria-label="row1"> + // <th>header_r1c1</th> <th>header_r1c2</th> <th>header_r1c3</th> + // </tr> + AXNodeData header_r1c1; + header_r1c1.id = 5; + header_r1c1.role = ax::mojom::Role::kColumnHeader; + header_r1c1.SetName(L"header_r1c1"); + row1.child_ids.push_back(header_r1c1.id); + + AXNodeData header_r1c2; + header_r1c2.id = 6; + header_r1c2.role = ax::mojom::Role::kColumnHeader; + header_r1c2.SetName(L"header_r1c2"); + row1.child_ids.push_back(header_r1c2.id); + + AXNodeData header_r1c3; + header_r1c3.id = 7; + header_r1c3.role = ax::mojom::Role::kColumnHeader; + header_r1c3.SetName(L"header_r1c3"); + row1.child_ids.push_back(header_r1c3.id); + + // <tr aria-label="row2"> + // <td>cell_r2c1</td> <td>cell_r2c2</td> <td>cell_r2c3</td> + // </tr> + AXNodeData cell_r2c1; + cell_r2c1.id = 8; + cell_r2c1.role = ax::mojom::Role::kCell; + cell_r2c1.SetName(L"cell_r2c1"); + row2.child_ids.push_back(cell_r2c1.id); + + AXNodeData cell_r2c2; + cell_r2c2.id = 9; + cell_r2c2.role = ax::mojom::Role::kCell; + cell_r2c2.SetName(L"cell_r2c2"); + row2.child_ids.push_back(cell_r2c2.id); + + AXNodeData cell_r2c3; + cell_r2c3.id = 10; + cell_r2c3.role = ax::mojom::Role::kCell; + cell_r2c3.SetName(L"cell_r2c3"); + row2.child_ids.push_back(cell_r2c3.id); + + // <tr aria-label="row3"> + // <td>cell_r3c1</td> <th>header_r3c2</th> + // </tr> + AXNodeData cell_r3c1; + cell_r3c1.id = 11; + cell_r3c1.role = ax::mojom::Role::kCell; + cell_r3c1.SetName(L"cell_r3c1"); + row3.child_ids.push_back(cell_r3c1.id); + + AXNodeData header_r3c2; + header_r3c2.id = 12; + header_r3c2.role = ax::mojom::Role::kColumnHeader; + header_r3c2.SetName(L"header_r3c2"); + row3.child_ids.push_back(header_r3c2.id); + + Init(root, row1, row2, row3, header_r1c1, header_r1c2, header_r1c3, cell_r2c1, + cell_r2c2, cell_r2c3, cell_r3c1, header_r3c2); + + ComPtr<ITableProvider> root_itableprovider( + QueryInterfaceFromNode<ITableProvider>(GetRootAsAXNode())); + + base::win::ScopedSafearray safearray; + EXPECT_HRESULT_SUCCEEDED( + root_itableprovider->GetColumnHeaders(safearray.Receive())); + EXPECT_NE(nullptr, safearray.Get()); + + // Validate that we retrieve all column headers of the table and in the order + // below. + std::vector<std::wstring> expected_names = {L"header_r1c1", L"header_r1c2", + L"header_r3c2", L"header_r1c3"}; + EXPECT_UIA_ELEMENT_ARRAY_BSTR_EQ(safearray.Get(), UIA_NamePropertyId, + expected_names); +} + TEST_F(AXPlatformNodeWinTest, ITableProviderGetRowHeaders) { AXNodeData root; root.id = 1; @@ -3690,6 +3917,7 @@ TEST_F(AXPlatformNodeWinTest, IA2GetAttribute) { TEST_F(AXPlatformNodeWinTest, UIAGetPropertySimple) { AXNodeData root; + root.role = ax::mojom::Role::kList; root.SetName("fake name"); root.AddStringAttribute(ax::mojom::StringAttribute::kAccessKey, "Ctrl+Q"); root.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en-us"); @@ -3699,7 +3927,6 @@ TEST_F(AXPlatformNodeWinTest, UIAGetPropertySimple) { root.AddIntAttribute(ax::mojom::IntAttribute::kSetSize, 2); root.AddIntAttribute(ax::mojom::IntAttribute::kInvalidState, 1); root.id = 1; - root.role = ax::mojom::Role::kList; AXNodeData child1; child1.id = 2; @@ -3725,7 +3952,8 @@ TEST_F(AXPlatformNodeWinTest, UIAGetPropertySimple) { EXPECT_UIA_BSTR_EQ(root_node, UIA_AriaPropertiesPropertyId, L"readonly=true;expanded=false;multiline=false;" L"multiselectable=false;required=false;setsize=2"); - EXPECT_UIA_BSTR_EQ(root_node, UIA_CulturePropertyId, L"en-us"); + constexpr int en_us_lcid = 1033; + EXPECT_UIA_INT_EQ(root_node, UIA_CulturePropertyId, en_us_lcid); EXPECT_UIA_BSTR_EQ(root_node, UIA_NamePropertyId, L"fake name"); EXPECT_UIA_INT_EQ(root_node, UIA_ControlTypePropertyId, int{UIA_ListControlTypeId}); @@ -3814,6 +4042,93 @@ TEST_F(AXPlatformNodeWinTest, UIAGetPropertyValueIsDialog) { UIA_IsDialogPropertyId, true); } +TEST_F(AXPlatformNodeWinTest, + UIAGetPropertyValueIsControlElementIgnoredInvisible) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {2, 3, 4, 5, 6, 7, 8}; + + AXNodeData normal_button; + normal_button.id = 2; + normal_button.role = ax::mojom::Role::kButton; + + AXNodeData ignored_button; + ignored_button.id = 3; + ignored_button.role = ax::mojom::Role::kButton; + ignored_button.AddState(ax::mojom::State::kIgnored); + + AXNodeData invisible_button; + invisible_button.id = 4; + invisible_button.role = ax::mojom::Role::kButton; + invisible_button.AddState(ax::mojom::State::kInvisible); + + AXNodeData invisible_focusable_button; + invisible_focusable_button.id = 5; + invisible_focusable_button.role = ax::mojom::Role::kButton; + invisible_focusable_button.AddState(ax::mojom::State::kInvisible); + invisible_focusable_button.AddState(ax::mojom::State::kFocusable); + + AXNodeData focusable_generic_container; + focusable_generic_container.id = 6; + focusable_generic_container.role = ax::mojom::Role::kGenericContainer; + focusable_generic_container.AddState(ax::mojom::State::kFocusable); + + AXNodeData ignored_focusable_generic_container; + ignored_focusable_generic_container.id = 7; + ignored_focusable_generic_container.role = ax::mojom::Role::kGenericContainer; + ignored_focusable_generic_container.AddState(ax::mojom::State::kIgnored); + focusable_generic_container.AddState(ax::mojom::State::kFocusable); + + AXNodeData invisible_focusable_generic_container; + invisible_focusable_generic_container.id = 8; + invisible_focusable_generic_container.role = + ax::mojom::Role::kGenericContainer; + invisible_focusable_generic_container.AddState(ax::mojom::State::kInvisible); + invisible_focusable_generic_container.AddState(ax::mojom::State::kFocusable); + + Init(root, normal_button, ignored_button, invisible_button, + invisible_focusable_button, focusable_generic_container, + ignored_focusable_generic_container, + invisible_focusable_generic_container); + + // Turn on web content mode for the AXTree. + TestAXNodeWrapper::SetGlobalIsWebContent(true); + + // Normal button (id=2), no invisible or ignored state set. Should be a + // control element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(0), + UIA_IsControlElementPropertyId, true); + + // Button with ignored state (id=3). Should not be a control element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(1), + UIA_IsControlElementPropertyId, false); + + // Button with invisible state (id=4). Should not be a control element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(2), + UIA_IsControlElementPropertyId, false); + + // Button with invisible state, but focusable (id=5). Should not be a control + // element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(3), + UIA_IsControlElementPropertyId, false); + + // Generic container, focusable (id=6). Should be a control + // element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(4), + UIA_IsControlElementPropertyId, true); + + // Generic container, ignored but focusable (id=7). Should not be a control + // element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(5), + UIA_IsControlElementPropertyId, false); + + // Generic container, invisible and ignored, but focusable (id=8). Should not + // be a control element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(6), + UIA_IsControlElementPropertyId, false); +} + TEST_F(AXPlatformNodeWinTest, UIAGetControllerForPropertyId) { AXNodeData root; root.id = 1; @@ -6251,6 +6566,65 @@ TEST_F(AXPlatformNodeWinTest, ISelectionItemProviderGetSelectionContainer) { EXPECT_EQ(container, container_provider); } +TEST_F(AXPlatformNodeWinTest, ISelectionItemProviderSelectFollowFocus) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kTabList; + + AXNodeData tab1; + tab1.id = 2; + tab1.role = ax::mojom::Role::kTab; + tab1.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, false); + tab1.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick); + root.child_ids.push_back(tab1.id); + + Init(root, tab1); + + auto* tab1_node = GetRootAsAXNode()->children()[0]; + ComPtr<IRawElementProviderSimple> tab1_raw_element_provider_simple = + QueryInterfaceFromNode<IRawElementProviderSimple>(tab1_node); + ASSERT_NE(nullptr, tab1_raw_element_provider_simple.Get()); + + ComPtr<IRawElementProviderFragment> tab1_raw_element_provider_fragment = + IRawElementProviderFragmentFromNode(tab1_node); + ASSERT_NE(nullptr, tab1_raw_element_provider_fragment.Get()); + + ComPtr<ISelectionItemProvider> tab1_selection_item_provider; + EXPECT_HRESULT_SUCCEEDED(tab1_raw_element_provider_simple->GetPatternProvider( + UIA_SelectionItemPatternId, &tab1_selection_item_provider)); + ASSERT_NE(nullptr, tab1_selection_item_provider.Get()); + + BOOL is_selected; + // Before setting focus to "tab1", validate that "tab1" has selected=false. + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_FALSE(is_selected); + + // Setting focus on "tab1" will result in selected=true. + tab1_raw_element_provider_fragment->SetFocus(); + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_TRUE(is_selected); + + // Verify that we can still trigger action::kDoDefault through Select(). + EXPECT_HRESULT_SUCCEEDED(tab1_selection_item_provider->Select()); + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_TRUE(is_selected); + EXPECT_EQ(tab1_node, TestAXNodeWrapper::GetNodeFromLastDefaultAction()); + // Verify that after Select(), "tab1" is still selected. + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_TRUE(is_selected); + + // Since last Select() performed |action::kDoDefault|, which set + // |kSelectedFromFocus| to false. Calling Select() again will not perform + // |action::kDoDefault| again. + TestAXNodeWrapper::SetNodeFromLastDefaultAction(nullptr); + EXPECT_HRESULT_SUCCEEDED(tab1_selection_item_provider->Select()); + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_TRUE(is_selected); + // Verify that after Select(),|action::kDoDefault| was not triggered on + // "tab1". + EXPECT_EQ(nullptr, TestAXNodeWrapper::GetNodeFromLastDefaultAction()); +} + TEST_F(AXPlatformNodeWinTest, IValueProvider_GetValue) { AXNodeData root; root.id = 1; @@ -6512,4 +6886,120 @@ TEST_F(AXPlatformNodeWinTest, SanitizeStringAttributeForIA2) { EXPECT_EQ("\\\\\\:\\=\\,\\;", output); } +// +// IChromeAccessible tests +// + +class TestIChromeAccessibleDelegate + : public CComObjectRootEx<CComMultiThreadModel>, + public IDispatchImpl<IChromeAccessibleDelegate> { + using IDispatchImpl::Invoke; + + public: + BEGIN_COM_MAP(TestIChromeAccessibleDelegate) + COM_INTERFACE_ENTRY(IChromeAccessibleDelegate) + END_COM_MAP() + + TestIChromeAccessibleDelegate() = default; + ~TestIChromeAccessibleDelegate() = default; + + std::string WaitForBulkFetchResult(LONG expected_request_id) { + if (bulk_fetch_result_.empty()) + WaitUsingRunLoop(); + CHECK_EQ(expected_request_id, request_id_); + return bulk_fetch_result_; + } + + IUnknown* WaitForHitTestResult(LONG expected_request_id) { + if (!hit_test_result_) + WaitUsingRunLoop(); + CHECK_EQ(expected_request_id, request_id_); + return hit_test_result_.Get(); + } + + private: + void WaitUsingRunLoop() { + base::RunLoop run_loop; + run_loop_quit_closure_ = run_loop.QuitClosure(); + run_loop.Run(); + } + + IFACEMETHODIMP put_bulkFetchResult(LONG request_id, BSTR result) override { + bulk_fetch_result_ = base::WideToUTF8(result); + request_id_ = request_id; + if (run_loop_quit_closure_) + run_loop_quit_closure_.Run(); + return S_OK; + } + + IFACEMETHODIMP put_hitTestResult(LONG request_id, IUnknown* result) override { + hit_test_result_ = result; + request_id_ = request_id; + if (run_loop_quit_closure_) + run_loop_quit_closure_.Run(); + return S_OK; + } + + std::string bulk_fetch_result_; + ComPtr<IUnknown> hit_test_result_; + LONG request_id_ = 0; + base::RepeatingClosure run_loop_quit_closure_; +}; + +// http://crbug.com/1087206: failing on Win7 builders. +TEST_F(AXPlatformNodeWinTest, DISABLED_BulkFetch) { + base::test::SingleThreadTaskEnvironment task_environment; + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kScrollBar; + + Init(root); + + ComPtr<IChromeAccessible> chrome_accessible = + QueryInterfaceFromNode<IChromeAccessible>(GetRootAsAXNode()); + + CComObject<TestIChromeAccessibleDelegate>* delegate = nullptr; + ASSERT_HRESULT_SUCCEEDED( + CComObject<TestIChromeAccessibleDelegate>::CreateInstance(&delegate)); + ScopedBstr input_bstr(L"Potato"); + chrome_accessible->get_bulkFetch(input_bstr.Get(), 99, delegate); + std::string response = delegate->WaitForBulkFetchResult(99); + + // Note: base::JSONReader is fine for unit tests, but production code + // that parses untrusted JSON should always use DataDecoder instead. + base::Optional<base::Value> result = + base::JSONReader::Read(response, base::JSON_ALLOW_TRAILING_COMMAS); + ASSERT_TRUE(result); + ASSERT_TRUE(result->FindKey("role")); + ASSERT_EQ("scrollBar", result->FindKey("role")->GetString()); +} + +TEST_F(AXPlatformNodeWinTest, AsyncHitTest) { + base::test::SingleThreadTaskEnvironment task_environment; + AXNodeData root; + root.id = 50; + root.role = ax::mojom::Role::kArticle; + root.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); + + Init(root); + + ComPtr<IChromeAccessible> chrome_accessible = + QueryInterfaceFromNode<IChromeAccessible>(GetRootAsAXNode()); + + CComObject<TestIChromeAccessibleDelegate>* delegate = nullptr; + ASSERT_HRESULT_SUCCEEDED( + CComObject<TestIChromeAccessibleDelegate>::CreateInstance(&delegate)); + ScopedBstr input_bstr(L"Potato"); + chrome_accessible->get_hitTest(400, 300, 12345, delegate); + ComPtr<IUnknown> result = delegate->WaitForHitTestResult(12345); + ComPtr<IAccessible2> accessible = ToIAccessible2(result); + LONG result_unique_id = 0; + ASSERT_HRESULT_SUCCEEDED(accessible->get_uniqueID(&result_unique_id)); + ComPtr<IAccessible2> root_accessible = + QueryInterfaceFromNode<IAccessible2>(GetRootAsAXNode()); + LONG root_unique_id = 0; + ASSERT_HRESULT_SUCCEEDED(root_accessible->get_uniqueID(&root_unique_id)); + ASSERT_EQ(root_unique_id, result_unique_id); +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h index 400be7c18c3..1fb54910d86 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h @@ -10,6 +10,7 @@ #include <memory> #include <unordered_set> +#include "base/test/scoped_feature_list.h" #include "ui/accessibility/platform/ax_fragment_root_delegate_win.h" #include "ui/base/win/accessibility_misc_utils.h" @@ -98,6 +99,8 @@ class AXPlatformNodeWinTest : public AXPlatformNodeTest { std::unique_ptr<AXFragmentRootWin> ax_fragment_root_; std::unique_ptr<TestFragmentRootDelegate> test_fragment_root_delegate_; + + base::test::ScopedFeatureList scoped_feature_list_; }; } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_relation_win.cc b/chromium/ui/accessibility/platform/ax_platform_relation_win.cc index 5f03ec806bf..eee92e3b9cb 100644 --- a/chromium/ui/accessibility/platform/ax_platform_relation_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_relation_win.cc @@ -43,6 +43,12 @@ base::string16 GetIA2RelationFromIntAttr(ax::mojom::IntAttribute attribute) { return IA2_RELATION_MEMBER_OF; case ax::mojom::IntAttribute::kErrormessageId: return IA2_RELATION_ERROR; + case ax::mojom::IntAttribute::kPopupForId: + // Map "popup for" to "controlled by". + // Unlike ATK there is no special IA2 popup-for relationship, but it can + // be exposed via the controlled by relation, which is also computed for + // content as the reverse of the controls relationship. + return IA2_RELATION_CONTROLLED_BY; default: break; } diff --git a/chromium/ui/accessibility/platform/ichromeaccessible.idl b/chromium/ui/accessibility/platform/ichromeaccessible.idl new file mode 100644 index 00000000000..f3567d1ba74 --- /dev/null +++ b/chromium/ui/accessibility/platform/ichromeaccessible.idl @@ -0,0 +1,64 @@ +// Copyright 2020 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. + +import "objidl.idl"; +import "oaidl.idl"; + +const long DISPID_CHROME_BULK_FETCH = -1600; +const long DISPID_CHROME_ON_BULK_FETCH_RESULT = -1601; +const long DISPID_CHROME_HIT_TEST = -1602; +const long DISPID_CHROME_ON_HIT_TEST_RESULT = -1603; + +// Interface to be implemented by the client that calls IChromeAccessible. +// For every method in IChromeAccessible, there's a corresponding response +// method in IChromeAccessibleDelegate. +[object, uuid(0e3edc14-79f4-413f-b854-d3b6860d74a2), pointer_default(unique)] +interface IChromeAccessibleDelegate : IUnknown +{ + [propput, id(DISPID_CHROME_ON_BULK_FETCH_RESULT)] HRESULT bulkFetchResult( + [in] LONG requestID, + [in] BSTR resultJson + ); + + [propput, id(DISPID_CHROME_ON_HIT_TEST_RESULT)] HRESULT hitTestResult( + [in] LONG requestID, + [in] IUnknown* result + ); +}; + +// Chrome-specific interface exposed on every IAccessible object. +// +// This interface is EXPERIMENTAL and only available behind a flag. +// Run Chrome with --enable-features=IChromeAccessible to use it. +// +// Do not depend on this interface remaining stable! It's only designed +// for prototyping ideas, and anything that's stabilized should move to +// an open standard API. +[object, uuid(6175bd95-3b2e-4ebc-bc51-9cab782bec92), pointer_default(unique)] +interface IChromeAccessible : IUnknown +{ + // TODO(crbug.com/1083834): Fully document this interface. + // Fetch multiple accessibility properties of one or more accessibility + // nodes as JSON. This method is asynchronous; the result is returned + // by calling put_bulkFetchResult on |delegate|. The client can pass any + // valid LONG as requestID and the same value will be passed to + // put_bulkFetchResult to enable matching of requests and responses. + [propget, id(DISPID_CHROME_BULK_FETCH)] HRESULT bulkFetch( + [in] BSTR inputJson, + [in] LONG requestID, + [in] IChromeAccessibleDelegate* delegate + ); + + // Hit-test the given pixel in screen physical pixel coordinates. + // This method is asynchronous; the result is returned + // by calling put_hitTestResult on |delegate|. The client can pass any + // valid LONG as requestID and the same value will be passed to + // put_hitTestResult to enable matching of requests and responses. + [propget, id(DISPID_CHROME_HIT_TEST)] HRESULT hitTest( + [in] LONG screenPhysicalPixelX, + [in] LONG screenPhysicalPixelY, + [in] LONG requestID, + [in] IChromeAccessibleDelegate* delegate + ); +}; diff --git a/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc b/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc index 85283d36f99..873ab5c1985 100644 --- a/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc +++ b/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc @@ -99,6 +99,11 @@ const AXNode* TestAXNodeWrapper::GetNodeFromLastDefaultAction() { } // static +void TestAXNodeWrapper::SetNodeFromLastDefaultAction(AXNode* node) { + g_node_from_last_default_action = node; +} + +// static std::unique_ptr<base::AutoReset<float>> TestAXNodeWrapper::SetScaleFactor( float value) { return std::make_unique<base::AutoReset<float>>(&g_scale_factor, value); @@ -459,30 +464,22 @@ base::Optional<bool> TestAXNodeWrapper::GetTableHasColumnOrRowHeaderNode() return node_->GetTableHasColumnOrRowHeaderNode(); } -std::vector<int32_t> TestAXNodeWrapper::GetColHeaderNodeIds() const { - std::vector<int32_t> header_ids; - node_->GetTableCellColHeaderNodeIds(&header_ids); - return header_ids; +std::vector<AXNode::AXID> TestAXNodeWrapper::GetColHeaderNodeIds() const { + return node_->GetTableColHeaderNodeIds(); } -std::vector<int32_t> TestAXNodeWrapper::GetColHeaderNodeIds( +std::vector<AXNode::AXID> TestAXNodeWrapper::GetColHeaderNodeIds( int col_index) const { - std::vector<int32_t> header_ids; - node_->GetTableColHeaderNodeIds(col_index, &header_ids); - return header_ids; + return node_->GetTableColHeaderNodeIds(col_index); } -std::vector<int32_t> TestAXNodeWrapper::GetRowHeaderNodeIds() const { - std::vector<int32_t> header_ids; - node_->GetTableCellRowHeaderNodeIds(&header_ids); - return header_ids; +std::vector<AXNode::AXID> TestAXNodeWrapper::GetRowHeaderNodeIds() const { + return node_->GetTableCellRowHeaderNodeIds(); } -std::vector<int32_t> TestAXNodeWrapper::GetRowHeaderNodeIds( +std::vector<AXNode::AXID> TestAXNodeWrapper::GetRowHeaderNodeIds( int row_index) const { - std::vector<int32_t> header_ids; - node_->GetTableRowHeaderNodeIds(row_index, &header_ids); - return header_ids; + return node_->GetTableRowHeaderNodeIds(row_index); } bool TestAXNodeWrapper::IsTableRow() const { @@ -586,6 +583,15 @@ bool TestAXNodeWrapper::AccessibilityPerformAction( } case ax::mojom::Action::kDoDefault: { + // If a default action such as a click is performed on an element, it + // could result in a selected state change. In which case, the element's + // selected state no longer comes from focus action, so we should set + // |kSelectedFromFocus| to false. + if (GetData().HasBoolAttribute( + ax::mojom::BoolAttribute::kSelectedFromFocus)) + ReplaceBoolAttribute(ax::mojom::BoolAttribute::kSelectedFromFocus, + false); + switch (GetData().role) { case ax::mojom::Role::kListBoxOption: case ax::mojom::Role::kCell: { @@ -611,7 +617,7 @@ bool TestAXNodeWrapper::AccessibilityPerformAction( default: break; } - g_node_from_last_default_action = node_; + SetNodeFromLastDefaultAction(node_); return true; } @@ -636,9 +642,21 @@ bool TestAXNodeWrapper::AccessibilityPerformAction( return true; } - case ax::mojom::Action::kFocus: + case ax::mojom::Action::kFocus: { g_focused_node_in_tree[tree_] = node_; + + // The platform has select follows focus behavior: + // https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_selection_follows_focus + // For test purpose, we support select follows focus for all elements, and + // not just single-selection container elements. + if (SupportsSelected(GetData().role)) { + ReplaceBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); + ReplaceBoolAttribute(ax::mojom::BoolAttribute::kSelectedFromFocus, + true); + } + return true; + } case ax::mojom::Action::kShowContextMenu: g_node_from_last_show_context_menu = node_; diff --git a/chromium/ui/accessibility/platform/test_ax_node_wrapper.h b/chromium/ui/accessibility/platform/test_ax_node_wrapper.h index 2f0998c7bab..4cd51d5874c 100644 --- a/chromium/ui/accessibility/platform/test_ax_node_wrapper.h +++ b/chromium/ui/accessibility/platform/test_ax_node_wrapper.h @@ -41,6 +41,10 @@ class TestAXNodeWrapper : public AXPlatformNodeDelegateBase { // called from for testing. static const AXNode* GetNodeFromLastDefaultAction(); + // Set the last node which AccessibilityPerformAction default action was + // called for testing. + static void SetNodeFromLastDefaultAction(AXNode* node); + // Set a global scale factor for testing. static std::unique_ptr<base::AutoReset<float>> SetScaleFactor(float value); diff --git a/chromium/ui/accessibility/platform/uia_registrar_win.cc b/chromium/ui/accessibility/platform/uia_registrar_win.cc new file mode 100644 index 00000000000..bd6ca8f56aa --- /dev/null +++ b/chromium/ui/accessibility/platform/uia_registrar_win.cc @@ -0,0 +1,50 @@ +// Copyright 2020 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 "ui/accessibility/platform/uia_registrar_win.h" +#include <wrl/implements.h> +#include "base/stl_util.h" + +namespace ui { + +UiaRegistrarWin::UiaRegistrarWin() { + // Create the registrar object and get the IUIAutomationRegistrar + // interface pointer. + Microsoft::WRL::ComPtr<IUIAutomationRegistrar> registrar; + if (FAILED(CoCreateInstance(CLSID_CUIAutomationRegistrar, nullptr, + CLSCTX_INPROC_SERVER, IID_IUIAutomationRegistrar, + ®istrar))) + return; + + // Register the custom UIA property that represents the unique id of an UIA + // element which also matches its corresponding IA2 element's unique id. + UIAutomationPropertyInfo unique_id_property_info = { + kUiaPropertyUniqueIdGuid, L"UniqueId", UIAutomationType_String}; + registrar->RegisterProperty(&unique_id_property_info, + &uia_unique_id_property_id_); + + // Register the custom UIA event that represents the test end event for the + // UIA test suite. + UIAutomationEventInfo test_complete_event_info = { + kUiaEventTestCompleteSentinelGuid, L"kUiaTestCompleteSentinel"}; + registrar->RegisterEvent(&test_complete_event_info, + &uia_test_complete_event_id_); +} + +UiaRegistrarWin::~UiaRegistrarWin() = default; + +PROPERTYID UiaRegistrarWin::GetUiaUniqueIdPropertyId() const { + return uia_unique_id_property_id_; +} + +EVENTID UiaRegistrarWin::GetUiaTestCompleteEventId() const { + return uia_test_complete_event_id_; +} + +const UiaRegistrarWin& UiaRegistrarWin::GetInstance() { + static base::NoDestructor<UiaRegistrarWin> instance; + return *instance; +} + +} // namespace ui diff --git a/chromium/ui/accessibility/platform/uia_registrar_win.h b/chromium/ui/accessibility/platform/uia_registrar_win.h new file mode 100644 index 00000000000..53c8da4fe37 --- /dev/null +++ b/chromium/ui/accessibility/platform/uia_registrar_win.h @@ -0,0 +1,45 @@ +// Copyright 2020 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. + +#ifndef UI_ACCESSIBILITY_PLATFORM_UIA_REGISTRAR_WIN_H_ +#define UI_ACCESSIBILITY_PLATFORM_UIA_REGISTRAR_WIN_H_ + +#include <objbase.h> +#include <uiautomation.h> +#include "base/macros.h" +#include "base/no_destructor.h" +#include "ui/accessibility/ax_export.h" + +namespace ui { +// {3761326A-34B2-465A-835D-7A3D8F4EFB92} +static const GUID kUiaEventTestCompleteSentinelGuid = { + 0x3761326a, + 0x34b2, + 0x465a, + {0x83, 0x5d, 0x7a, 0x3d, 0x8f, 0x4e, 0xfb, 0x92}}; + +// {cc7eeb32-4b62-4f4c-aff6-1c2e5752ad8e} +static const GUID kUiaPropertyUniqueIdGuid = { + 0xcc7eeb32, + 0x4b62, + 0x4f4c, + {0xaf, 0xf6, 0x1c, 0x2e, 0x57, 0x52, 0xad, 0x8e}}; + +class AX_EXPORT UiaRegistrarWin { + public: + UiaRegistrarWin(); + ~UiaRegistrarWin(); + PROPERTYID GetUiaUniqueIdPropertyId() const; + EVENTID GetUiaTestCompleteEventId() const; + + static const UiaRegistrarWin& GetInstance(); + + private: + PROPERTYID uia_unique_id_property_id_ = 0; + EVENTID uia_test_complete_event_id_ = 0; +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_PLATFORM_UIA_REGISTRAR_WIN_H_ diff --git a/chromium/ui/accessibility/test_ax_node_helper.cc b/chromium/ui/accessibility/test_ax_node_helper.cc new file mode 100644 index 00000000000..ba257f7fffe --- /dev/null +++ b/chromium/ui/accessibility/test_ax_node_helper.cc @@ -0,0 +1,204 @@ +// Copyright 2020 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 "ui/accessibility/test_ax_node_helper.h" + +#include <map> +#include <utility> + +#include "base/numerics/ranges.h" +#include "base/stl_util.h" +#include "base/strings/utf_string_conversions.h" +#include "ui/accessibility/ax_action_data.h" +#include "ui/accessibility/ax_role_properties.h" +#include "ui/accessibility/ax_table_info.h" +#include "ui/accessibility/ax_tree_observer.h" +#include "ui/gfx/geometry/rect_conversions.h" + +namespace ui { + +namespace { + +// A global map from AXNodes to TestAXNodeHelpers. +std::map<AXNode::AXID, TestAXNodeHelper*> g_node_id_to_helper_map; + +// A simple implementation of AXTreeObserver to catch when AXNodes are +// deleted so we can delete their helpers. +class TestAXTreeObserver : public AXTreeObserver { + private: + void OnNodeDeleted(AXTree* tree, int32_t node_id) override { + const auto iter = g_node_id_to_helper_map.find(node_id); + if (iter != g_node_id_to_helper_map.end()) { + TestAXNodeHelper* helper = iter->second; + delete helper; + g_node_id_to_helper_map.erase(node_id); + } + } +}; + +TestAXTreeObserver g_ax_tree_observer; + +} // namespace + +// static +TestAXNodeHelper* TestAXNodeHelper::GetOrCreate(AXTree* tree, AXNode* node) { + if (!tree || !node) + return nullptr; + + if (!tree->HasObserver(&g_ax_tree_observer)) + tree->AddObserver(&g_ax_tree_observer); + auto iter = g_node_id_to_helper_map.find(node->id()); + if (iter != g_node_id_to_helper_map.end()) + return iter->second; + TestAXNodeHelper* helper = new TestAXNodeHelper(tree, node); + g_node_id_to_helper_map[node->id()] = helper; + return helper; +} + +TestAXNodeHelper::TestAXNodeHelper(AXTree* tree, AXNode* node) + : tree_(tree), node_(node) {} + +TestAXNodeHelper::~TestAXNodeHelper() = default; + +gfx::Rect TestAXNodeHelper::GetBoundsRect( + const AXCoordinateSystem coordinate_system, + const AXClippingBehavior clipping_behavior, + AXOffscreenResult* offscreen_result) const { + switch (coordinate_system) { + case AXCoordinateSystem::kScreenPhysicalPixels: + // For unit testing purposes, assume a device scale factor of 1 and fall + // through. + case AXCoordinateSystem::kScreenDIPs: { + // We could optionally add clipping here if ever needed. + gfx::RectF bounds = GetLocation(); + + // For test behavior only, for bounds that are offscreen we currently do + // not apply clipping to the bounds but we still return the offscreen + // status. + if (offscreen_result) { + *offscreen_result = DetermineOffscreenResult(bounds); + } + + return gfx::ToEnclosingRect(bounds); + } + case AXCoordinateSystem::kRootFrame: + case AXCoordinateSystem::kFrame: + NOTIMPLEMENTED(); + return gfx::Rect(); + } +} + +gfx::Rect TestAXNodeHelper::GetInnerTextRangeBoundsRect( + const int start_offset, + const int end_offset, + const AXCoordinateSystem coordinate_system, + const AXClippingBehavior clipping_behavior, + AXOffscreenResult* offscreen_result) const { + switch (coordinate_system) { + case AXCoordinateSystem::kScreenPhysicalPixels: + // For unit testing purposes, assume a device scale factor of 1 and fall + // through. + case AXCoordinateSystem::kScreenDIPs: { + gfx::RectF bounds = GetLocation(); + // This implementation currently only deals with text node that has role + // kInlineTextBox and kStaticText. + // For test purposes, assume node with kStaticText always has a single + // child with role kInlineTextBox. + if (GetData().role == ax::mojom::Role::kInlineTextBox) { + bounds = GetInlineTextRect(start_offset, end_offset); + } else if (GetData().role == ax::mojom::Role::kStaticText && + InternalChildCount() > 0) { + TestAXNodeHelper* child = InternalGetChild(0); + if (child != nullptr && + child->GetData().role == ax::mojom::Role::kInlineTextBox) { + bounds = child->GetInlineTextRect(start_offset, end_offset); + } + } + + // For test behavior only, for bounds that are offscreen we currently do + // not apply clipping to the bounds but we still return the offscreen + // status. + if (offscreen_result) { + *offscreen_result = DetermineOffscreenResult(bounds); + } + + return gfx::ToEnclosingRect(bounds); + } + case AXCoordinateSystem::kRootFrame: + case AXCoordinateSystem::kFrame: + NOTIMPLEMENTED(); + return gfx::Rect(); + } +} + +const AXNodeData& TestAXNodeHelper::GetData() const { + return node_->data(); +} + +gfx::RectF TestAXNodeHelper::GetLocation() const { + return GetData().relative_bounds.bounds; +} + +int TestAXNodeHelper::InternalChildCount() const { + return int{node_->GetUnignoredChildCount()}; +} + +TestAXNodeHelper* TestAXNodeHelper::InternalGetChild(int index) const { + CHECK_GE(index, 0); + CHECK_LT(index, InternalChildCount()); + return GetOrCreate(tree_, node_->GetUnignoredChildAtIndex(size_t{index})); +} + +gfx::RectF TestAXNodeHelper::GetInlineTextRect(const int start_offset, + const int end_offset) const { + DCHECK(start_offset >= 0 && end_offset >= 0 && start_offset <= end_offset); + const std::vector<int32_t>& character_offsets = GetData().GetIntListAttribute( + ax::mojom::IntListAttribute::kCharacterOffsets); + gfx::RectF location = GetLocation(); + gfx::RectF bounds; + + switch (static_cast<ax::mojom::TextDirection>( + GetData().GetIntAttribute(ax::mojom::IntAttribute::kTextDirection))) { + // Currently only kNone and kLtr are supported text direction. + case ax::mojom::TextDirection::kNone: + case ax::mojom::TextDirection::kLtr: { + int start_pixel_offset = + start_offset > 0 ? character_offsets[start_offset - 1] : location.x(); + int end_pixel_offset = + end_offset > 0 ? character_offsets[end_offset - 1] : location.x(); + bounds = + gfx::RectF(start_pixel_offset, location.y(), + end_pixel_offset - start_pixel_offset, location.height()); + break; + } + default: + NOTIMPLEMENTED(); + } + return bounds; +} + +AXOffscreenResult TestAXNodeHelper::DetermineOffscreenResult( + gfx::RectF bounds) const { + if (!tree_ || !tree_->root()) + return AXOffscreenResult::kOnscreen; + + const AXNodeData& root_web_area_node_data = tree_->root()->data(); + gfx::RectF root_web_area_bounds = + root_web_area_node_data.relative_bounds.bounds; + + // For testing, we only look at the current node's bound relative to the root + // web area bounds to determine offscreen status. We currently do not look at + // the bounds of the immediate parent of the node for determining offscreen + // status. + // We only determine offscreen result if the root web area bounds is actually + // set in the test. We default the offscreen result of every other situation + // to AXOffscreenResult::kOnscreen. + if (!root_web_area_bounds.IsEmpty()) { + bounds.Intersect(root_web_area_bounds); + if (bounds.IsEmpty()) + return AXOffscreenResult::kOffscreen; + } + return AXOffscreenResult::kOnscreen; +} +} // namespace ui diff --git a/chromium/ui/accessibility/test_ax_node_helper.h b/chromium/ui/accessibility/test_ax_node_helper.h new file mode 100644 index 00000000000..a303b81abf9 --- /dev/null +++ b/chromium/ui/accessibility/test_ax_node_helper.h @@ -0,0 +1,50 @@ +// Copyright 2020 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. + +#ifndef UI_ACCESSIBILITY_TEST_AX_NODE_HELPER_H_ +#define UI_ACCESSIBILITY_TEST_AX_NODE_HELPER_H_ + +#include "ui/accessibility/ax_clipping_behavior.h" +#include "ui/accessibility/ax_coordinate_system.h" +#include "ui/accessibility/ax_node.h" +#include "ui/accessibility/ax_offscreen_result.h" +#include "ui/accessibility/ax_tree.h" + +namespace ui { + +// For testing, a TestAXNodeHelper wraps an AXNode. This is a simple +// version of TestAXNodeWrapper. +class TestAXNodeHelper { + public: + // Create TestAXNodeHelper instances on-demand from an AXTree and AXNode. + static TestAXNodeHelper* GetOrCreate(AXTree* tree, AXNode* node); + ~TestAXNodeHelper(); + + gfx::Rect GetBoundsRect(const AXCoordinateSystem coordinate_system, + const AXClippingBehavior clipping_behavior, + AXOffscreenResult* offscreen_result) const; + gfx::Rect GetInnerTextRangeBoundsRect( + const int start_offset, + const int end_offset, + const AXCoordinateSystem coordinate_system, + const AXClippingBehavior clipping_behavior, + AXOffscreenResult* offscreen_result) const; + + private: + TestAXNodeHelper(AXTree* tree, AXNode* node); + int InternalChildCount() const; + TestAXNodeHelper* InternalGetChild(int index) const; + const AXNodeData& GetData() const; + gfx::RectF GetLocation() const; + gfx::RectF GetInlineTextRect(const int start_offset, + const int end_offset) const; + AXOffscreenResult DetermineOffscreenResult(gfx::RectF bounds) const; + + AXTree* tree_; + AXNode* node_; +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_TEST_AX_NODE_HELPER_H_ |