Commit 6570cd5e authored by Hiroki Sato's avatar Hiroki Sato Committed by Commit Bot

arc-a11y: Improve focusability computation

This CL tries to make linear navigation order of ChromeVox on ARC++
similar to TalkBack.

For example, imagine a11y tree in Android is like this:
++ nodeA container (focusable clickable) (name is empty)
++++ nodeB static text (not focusable, not clickable) (name X)
++++ nodeC button (focusable clickable) (name Y)

TalkBack's navigation order is as below:
1. nodeA, announce a name of nodeB.
2. nodeC, announce a name of nodeC.

Currently ChromeVox skips nodeA. With this CL, ChromeVox firstly focus
on nodeA and nodeB is skipped because it's ignored.

This CL introcues 'screen reader mode' in ARC accessibility, which is
enabled when SwitchAccess or ChromeVox is enabled. The focusability
change added in this CL is applied only when the screen reader mode is
enabled so that we don't break text location API for Select-to-Speak.

Note that this change adds a few TODOs, which will be resolved in follow
up CLs.

AX-Relnotes: n/a.
Bug: b:157441336
Bug: b:150344900
Bug: b:150344310
Test: unit_tests --gtest_filter="AXTreeSourceArcTest.*"
Test: manually confirmed with PlayStore and Android Settings.
Change-Id: I700cbbd348c9eb7bf71a7995994c926b1c839979
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2222068
Commit-Queue: Hiroki Sato <hirokisato@chromium.org>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Reviewed-by: default avatarSara Kato <sarakato@chromium.org>
Cr-Commit-Position: refs/heads/master@{#778595}
parent c190f15f
...@@ -37,7 +37,9 @@ class AccessibilityInfoDataWrapper { ...@@ -37,7 +37,9 @@ class AccessibilityInfoDataWrapper {
virtual const gfx::Rect GetBounds() const = 0; virtual const gfx::Rect GetBounds() const = 0;
virtual bool IsVisibleToUser() const = 0; virtual bool IsVisibleToUser() const = 0;
virtual bool IsVirtualNode() const = 0; virtual bool IsVirtualNode() const = 0;
virtual bool IsIgnored() const = 0;
virtual bool CanBeAccessibilityFocused() const = 0; virtual bool CanBeAccessibilityFocused() const = 0;
virtual bool IsAccessibilityFocusableContainer() const = 0;
virtual void PopulateAXRole(ui::AXNodeData* out_data) const = 0; virtual void PopulateAXRole(ui::AXNodeData* out_data) const = 0;
virtual void PopulateAXState(ui::AXNodeData* out_data) const = 0; virtual void PopulateAXState(ui::AXNodeData* out_data) const = 0;
virtual void Serialize(ui::AXNodeData* out_data) const = 0; virtual void Serialize(ui::AXNodeData* out_data) const = 0;
......
...@@ -74,7 +74,40 @@ bool AccessibilityNodeInfoDataWrapper::IsVirtualNode() const { ...@@ -74,7 +74,40 @@ bool AccessibilityNodeInfoDataWrapper::IsVirtualNode() const {
return node_ptr_->is_virtual_node; return node_ptr_->is_virtual_node;
} }
bool AccessibilityNodeInfoDataWrapper::IsIgnored() const {
if (!tree_source_->IsScreenReaderMode())
return !is_important_;
// Most conditions are precomputed in AXTreeSourceArc::BuildImportanceTable().
// TODO(hirokisato): migrate importance computation here.
if (!is_important_)
return true;
if (IsAccessibilityFocusableContainer())
return false;
// On screen reader mode, text might be used by focusable ancestor.
// Make such nodes ignored. See ComputeNameFromContents().
bool has_text = HasNonEmptyStringProperty(
node_ptr_, AXStringProperty::CONTENT_DESCRIPTION) ||
HasNonEmptyStringProperty(node_ptr_, AXStringProperty::TEXT);
if (!has_text)
return false;
AccessibilityInfoDataWrapper* parent = tree_source_->GetParent(
const_cast<AccessibilityNodeInfoDataWrapper*>(this));
while (parent && parent->IsNode()) {
if (parent->IsAccessibilityFocusableContainer())
return true;
parent = tree_source_->GetParent(parent);
}
return false;
}
bool AccessibilityNodeInfoDataWrapper::CanBeAccessibilityFocused() const { bool AccessibilityNodeInfoDataWrapper::CanBeAccessibilityFocused() const {
// TODO(hirokisato): Remove this method and only use
// IsAccessibilityFocusableContainer().
// An important node with a non-generic role and: // An important node with a non-generic role and:
// - actionable nodes // - actionable nodes
// - top level scrollables with a name // - top level scrollables with a name
...@@ -90,10 +123,20 @@ bool AccessibilityNodeInfoDataWrapper::CanBeAccessibilityFocused() const { ...@@ -90,10 +123,20 @@ bool AccessibilityNodeInfoDataWrapper::CanBeAccessibilityFocused() const {
GetProperty(AXBooleanProperty::CHECKABLE); GetProperty(AXBooleanProperty::CHECKABLE);
bool top_level_scrollable = HasProperty(AXStringProperty::TEXT) && bool top_level_scrollable = HasProperty(AXStringProperty::TEXT) &&
GetProperty(AXBooleanProperty::SCROLLABLE); GetProperty(AXBooleanProperty::SCROLLABLE);
return is_important_ && non_generic_role && return !IsIgnored() && non_generic_role &&
(actionable || top_level_scrollable || IsInterestingLeaf()); (actionable || top_level_scrollable || IsInterestingLeaf());
} }
bool AccessibilityNodeInfoDataWrapper::IsAccessibilityFocusableContainer()
const {
if (!is_important_)
return false;
return GetProperty(AXBooleanProperty::SCREEN_READER_FOCUSABLE) ||
GetProperty(AXBooleanProperty::CLICKABLE) ||
GetProperty(AXBooleanProperty::FOCUSABLE) || IsToplevelScrollItem();
}
void AccessibilityNodeInfoDataWrapper::PopulateAXRole( void AccessibilityNodeInfoDataWrapper::PopulateAXRole(
ui::AXNodeData* out_data) const { ui::AXNodeData* out_data) const {
std::string class_name; std::string class_name;
...@@ -244,6 +287,13 @@ void AccessibilityNodeInfoDataWrapper::PopulateAXRole( ...@@ -244,6 +287,13 @@ void AccessibilityNodeInfoDataWrapper::PopulateAXRole(
#undef MAP_ROLE #undef MAP_ROLE
if (node_ptr_->collection_info) {
// Fallback for some RecyclerViews which doesn't correctly populate
// row/col counts.
out_data->role = ax::mojom::Role::kList;
return;
}
std::string text; std::string text;
GetProperty(AXStringProperty::TEXT, &text); GetProperty(AXStringProperty::TEXT, &text);
std::vector<AccessibilityInfoDataWrapper*> children; std::vector<AccessibilityInfoDataWrapper*> children;
...@@ -264,12 +314,17 @@ void AccessibilityNodeInfoDataWrapper::PopulateAXState( ...@@ -264,12 +314,17 @@ void AccessibilityNodeInfoDataWrapper::PopulateAXState(
// BrowserAccessibilityAndroid. They do not completely match the above two // BrowserAccessibilityAndroid. They do not completely match the above two
// sources. // sources.
MAP_STATE(AXBooleanProperty::EDITABLE, ax::mojom::State::kEditable); MAP_STATE(AXBooleanProperty::EDITABLE, ax::mojom::State::kEditable);
MAP_STATE(AXBooleanProperty::FOCUSABLE, ax::mojom::State::kFocusable);
MAP_STATE(AXBooleanProperty::MULTI_LINE, ax::mojom::State::kMultiline); MAP_STATE(AXBooleanProperty::MULTI_LINE, ax::mojom::State::kMultiline);
MAP_STATE(AXBooleanProperty::PASSWORD, ax::mojom::State::kProtected); MAP_STATE(AXBooleanProperty::PASSWORD, ax::mojom::State::kProtected);
#undef MAP_STATE #undef MAP_STATE
const bool focusable = tree_source_->IsScreenReaderMode()
? IsAccessibilityFocusableContainer()
: GetProperty(AXBooleanProperty::FOCUSABLE);
if (focusable)
out_data->AddState(ax::mojom::State::kFocusable);
if (GetProperty(AXBooleanProperty::CHECKABLE)) { if (GetProperty(AXBooleanProperty::CHECKABLE)) {
const bool is_checked = GetProperty(AXBooleanProperty::CHECKED); const bool is_checked = GetProperty(AXBooleanProperty::CHECKED);
out_data->SetCheckedState(is_checked ? ax::mojom::CheckedState::kTrue out_data->SetCheckedState(is_checked ? ax::mojom::CheckedState::kTrue
...@@ -282,7 +337,7 @@ void AccessibilityNodeInfoDataWrapper::PopulateAXState( ...@@ -282,7 +337,7 @@ void AccessibilityNodeInfoDataWrapper::PopulateAXState(
if (!GetProperty(AXBooleanProperty::VISIBLE_TO_USER)) if (!GetProperty(AXBooleanProperty::VISIBLE_TO_USER))
out_data->AddState(ax::mojom::State::kInvisible); out_data->AddState(ax::mojom::State::kInvisible);
if (!is_important_) if (IsIgnored())
out_data->AddState(ax::mojom::State::kIgnored); out_data->AddState(ax::mojom::State::kIgnored);
} }
...@@ -360,10 +415,13 @@ void AccessibilityNodeInfoDataWrapper::Serialize( ...@@ -360,10 +415,13 @@ void AccessibilityNodeInfoDataWrapper::Serialize(
// from Talkback. // from Talkback.
if (!names.empty()) if (!names.empty())
out_data->SetName(base::JoinString(names, " ")); out_data->SetName(base::JoinString(names, " "));
} else if (is_clickable_leaf_) { } else if (tree_source_->IsScreenReaderMode() &&
IsAccessibilityFocusableContainer()) {
// Compute the name by joining all nodes with names. // Compute the name by joining all nodes with names.
// When this is clickable leaf, compute from all the nodes.
// Otherwise, compute from only non-clickable / non-focusable nodes.
std::vector<std::string> names; std::vector<std::string> names;
ComputeNameFromContents(this, &names); ComputeNameFromContents(!is_clickable_leaf_, &names);
if (!names.empty()) if (!names.empty())
out_data->SetName(base::JoinString(names, " ")); out_data->SetName(base::JoinString(names, " "));
} }
...@@ -586,14 +644,29 @@ bool AccessibilityNodeInfoDataWrapper::HasCoveringSpan( ...@@ -586,14 +644,29 @@ bool AccessibilityNodeInfoDataWrapper::HasCoveringSpan(
} }
void AccessibilityNodeInfoDataWrapper::ComputeNameFromContents( void AccessibilityNodeInfoDataWrapper::ComputeNameFromContents(
const AccessibilityNodeInfoDataWrapper* data, bool from_non_focusables,
std::vector<std::string>* names) const {
std::vector<AccessibilityInfoDataWrapper*> children;
GetChildren(&children);
for (AccessibilityInfoDataWrapper* child : children) {
static_cast<AccessibilityNodeInfoDataWrapper*>(child)
->ComputeNameFromContentsInternal(from_non_focusables, names);
}
}
void AccessibilityNodeInfoDataWrapper::ComputeNameFromContentsInternal(
bool from_non_focusables,
std::vector<std::string>* names) const { std::vector<std::string>* names) const {
if (from_non_focusables && IsAccessibilityFocusableContainer())
return;
// Take the name from either content description or text. It's not clear // Take the name from either content description or text. It's not clear
// whether labelled by should be taken into account here. // whether labelled by should be taken into account here.
std::string name; std::string name;
if (!data->GetProperty(AXStringProperty::CONTENT_DESCRIPTION, &name) || if (!GetProperty(AXStringProperty::CONTENT_DESCRIPTION, &name) ||
name.empty()) name.empty()) {
data->GetProperty(AXStringProperty::TEXT, &name); GetProperty(AXStringProperty::TEXT, &name);
}
// Stop when we get a name for this subtree. // Stop when we get a name for this subtree.
if (!name.empty()) { if (!name.empty()) {
...@@ -603,13 +676,38 @@ void AccessibilityNodeInfoDataWrapper::ComputeNameFromContents( ...@@ -603,13 +676,38 @@ void AccessibilityNodeInfoDataWrapper::ComputeNameFromContents(
// Otherwise, continue looking for a name in this subtree. // Otherwise, continue looking for a name in this subtree.
std::vector<AccessibilityInfoDataWrapper*> children; std::vector<AccessibilityInfoDataWrapper*> children;
data->GetChildren(&children); GetChildren(&children);
for (AccessibilityInfoDataWrapper* child : children) { for (AccessibilityInfoDataWrapper* child : children) {
ComputeNameFromContents( static_cast<AccessibilityNodeInfoDataWrapper*>(child)
static_cast<AccessibilityNodeInfoDataWrapper*>(child), names); ->ComputeNameFromContentsInternal(from_non_focusables, names);
} }
} }
bool AccessibilityNodeInfoDataWrapper::IsScrollableContainer() const {
if (GetProperty(AXBooleanProperty::SCROLLABLE))
return true;
ui::AXNodeData data;
PopulateAXRole(&data);
return data.role == ax::mojom::Role::kList ||
data.role == ax::mojom::Role::kGrid ||
data.role == ax::mojom::Role::kScrollView;
}
bool AccessibilityNodeInfoDataWrapper::IsToplevelScrollItem() const {
if (!IsVisibleToUser())
return false;
AccessibilityInfoDataWrapper* parent =
tree_source_->GetFirstImportantAncestor(
const_cast<AccessibilityNodeInfoDataWrapper*>(this));
if (!parent || !parent->IsNode())
return false;
return static_cast<AccessibilityNodeInfoDataWrapper*>(parent)
->IsScrollableContainer();
}
bool AccessibilityNodeInfoDataWrapper::IsInterestingLeaf() const { bool AccessibilityNodeInfoDataWrapper::IsInterestingLeaf() const {
std::vector<AccessibilityInfoDataWrapper*> children; std::vector<AccessibilityInfoDataWrapper*> children;
GetChildren(&children); GetChildren(&children);
......
...@@ -34,7 +34,9 @@ class AccessibilityNodeInfoDataWrapper : public AccessibilityInfoDataWrapper { ...@@ -34,7 +34,9 @@ class AccessibilityNodeInfoDataWrapper : public AccessibilityInfoDataWrapper {
const gfx::Rect GetBounds() const override; const gfx::Rect GetBounds() const override;
bool IsVisibleToUser() const override; bool IsVisibleToUser() const override;
bool IsVirtualNode() const override; bool IsVirtualNode() const override;
bool IsIgnored() const override;
bool CanBeAccessibilityFocused() const override; bool CanBeAccessibilityFocused() const override;
bool IsAccessibilityFocusableContainer() const override;
void PopulateAXRole(ui::AXNodeData* out_data) const override; void PopulateAXRole(ui::AXNodeData* out_data) const override;
void PopulateAXState(ui::AXNodeData* out_data) const override; void PopulateAXState(ui::AXNodeData* out_data) const override;
void Serialize(ui::AXNodeData* out_data) const override; void Serialize(ui::AXNodeData* out_data) const override;
...@@ -66,9 +68,14 @@ class AccessibilityNodeInfoDataWrapper : public AccessibilityInfoDataWrapper { ...@@ -66,9 +68,14 @@ class AccessibilityNodeInfoDataWrapper : public AccessibilityInfoDataWrapper {
bool HasCoveringSpan(mojom::AccessibilityStringProperty prop, bool HasCoveringSpan(mojom::AccessibilityStringProperty prop,
mojom::SpanType span_type) const; mojom::SpanType span_type) const;
void ComputeNameFromContents(const AccessibilityNodeInfoDataWrapper* data, void ComputeNameFromContents(bool from_non_focusables,
std::vector<std::string>* names) const; std::vector<std::string>* names) const;
void ComputeNameFromContentsInternal(bool from_non_focusables,
std::vector<std::string>* names) const;
bool IsScrollableContainer() const;
bool IsToplevelScrollItem() const;
bool IsInterestingLeaf() const; bool IsInterestingLeaf() const;
mojom::AccessibilityNodeInfoData* node_ptr_ = nullptr; mojom::AccessibilityNodeInfoData* node_ptr_ = nullptr;
......
...@@ -48,6 +48,10 @@ bool AccessibilityWindowInfoDataWrapper::IsVirtualNode() const { ...@@ -48,6 +48,10 @@ bool AccessibilityWindowInfoDataWrapper::IsVirtualNode() const {
return false; return false;
} }
bool AccessibilityWindowInfoDataWrapper::IsIgnored() const {
return false;
}
bool AccessibilityWindowInfoDataWrapper::CanBeAccessibilityFocused() const { bool AccessibilityWindowInfoDataWrapper::CanBeAccessibilityFocused() const {
// Windows are too generic to be Accessibility focused in Chrome, although // Windows are too generic to be Accessibility focused in Chrome, although
// they can be Accessibility focused in Android by virtue of having // they can be Accessibility focused in Android by virtue of having
...@@ -55,6 +59,11 @@ bool AccessibilityWindowInfoDataWrapper::CanBeAccessibilityFocused() const { ...@@ -55,6 +59,11 @@ bool AccessibilityWindowInfoDataWrapper::CanBeAccessibilityFocused() const {
return false; return false;
} }
bool AccessibilityWindowInfoDataWrapper::IsAccessibilityFocusableContainer()
const {
return tree_source_->GetRoot()->GetId() == GetId();
}
void AccessibilityWindowInfoDataWrapper::PopulateAXRole( void AccessibilityWindowInfoDataWrapper::PopulateAXRole(
ui::AXNodeData* out_data) const { ui::AXNodeData* out_data) const {
switch (window_ptr_->window_type) { switch (window_ptr_->window_type) {
......
...@@ -30,7 +30,9 @@ class AccessibilityWindowInfoDataWrapper : public AccessibilityInfoDataWrapper { ...@@ -30,7 +30,9 @@ class AccessibilityWindowInfoDataWrapper : public AccessibilityInfoDataWrapper {
const gfx::Rect GetBounds() const override; const gfx::Rect GetBounds() const override;
bool IsVisibleToUser() const override; bool IsVisibleToUser() const override;
bool IsVirtualNode() const override; bool IsVirtualNode() const override;
bool IsIgnored() const override;
bool CanBeAccessibilityFocused() const override; bool CanBeAccessibilityFocused() const override;
bool IsAccessibilityFocusableContainer() const override;
void PopulateAXRole(ui::AXNodeData* out_data) const override; void PopulateAXRole(ui::AXNodeData* out_data) const override;
void PopulateAXState(ui::AXNodeData* out_data) const override; void PopulateAXState(ui::AXNodeData* out_data) const override;
void Serialize(ui::AXNodeData* out_data) const override; void Serialize(ui::AXNodeData* out_data) const override;
......
...@@ -517,6 +517,10 @@ void ArcAccessibilityHelperBridge::OnAction( ...@@ -517,6 +517,10 @@ void ArcAccessibilityHelperBridge::OnAction(
base::Unretained(this), data)); base::Unretained(this), data));
} }
bool ArcAccessibilityHelperBridge::IsScreenReaderEnabled() const {
return is_screen_reader_enabled_;
}
void ArcAccessibilityHelperBridge::OnTaskDestroyed(int32_t task_id) { void ArcAccessibilityHelperBridge::OnTaskDestroyed(int32_t task_id) {
trees_.erase(KeyForTaskId(task_id)); trees_.erase(KeyForTaskId(task_id));
} }
...@@ -699,11 +703,15 @@ void ArcAccessibilityHelperBridge::UpdateEnabledFeature() { ...@@ -699,11 +703,15 @@ void ArcAccessibilityHelperBridge::UpdateEnabledFeature() {
if (instance) if (instance)
instance->SetFilter(filter_type_); instance->SetFilter(filter_type_);
if (!chromeos::AccessibilityManager::Get()) chromeos::AccessibilityManager* accessibility_manager =
chromeos::AccessibilityManager::Get();
if (!accessibility_manager)
return; return;
is_focus_highlight_enabled_ = is_focus_highlight_enabled_ =
filter_type_ != arc::mojom::AccessibilityFilterType::OFF && accessibility_manager->IsFocusHighlightEnabled();
chromeos::AccessibilityManager::Get()->IsFocusHighlightEnabled(); is_screen_reader_enabled_ = accessibility_manager->IsSwitchAccessEnabled() ||
accessibility_manager->IsSpokenFeedbackEnabled();
bool add_activation_observer = bool add_activation_observer =
filter_type_ == arc::mojom::AccessibilityFilterType::ALL; filter_type_ == arc::mojom::AccessibilityFilterType::ALL;
......
...@@ -96,6 +96,7 @@ class ArcAccessibilityHelperBridge ...@@ -96,6 +96,7 @@ class ArcAccessibilityHelperBridge
// AXTreeSourceArc::Delegate overrides. // AXTreeSourceArc::Delegate overrides.
void OnAction(const ui::AXActionData& data) const override; void OnAction(const ui::AXActionData& data) const override;
bool IsScreenReaderEnabled() const override;
// ArcAppListPrefs::Observer overrides. // ArcAppListPrefs::Observer overrides.
void OnTaskDestroyed(int32_t task_id) override; void OnTaskDestroyed(int32_t task_id) override;
...@@ -167,6 +168,7 @@ class ArcAccessibilityHelperBridge ...@@ -167,6 +168,7 @@ class ArcAccessibilityHelperBridge
bool activation_observer_added_ = false; bool activation_observer_added_ = false;
bool is_focus_highlight_enabled_ = false; bool is_focus_highlight_enabled_ = false;
bool is_screen_reader_enabled_ = false;
Profile* const profile_; Profile* const profile_;
ArcBridgeService* const arc_bridge_service_; ArcBridgeService* const arc_bridge_service_;
TreeMap trees_; TreeMap trees_;
......
...@@ -198,6 +198,10 @@ void AXTreeSourceArc::NotifyGetTextLocationDataResult( ...@@ -198,6 +198,10 @@ void AXTreeSourceArc::NotifyGetTextLocationDataResult(
GetAutomationEventRouter()->DispatchGetTextLocationDataResult(data, rect); GetAutomationEventRouter()->DispatchGetTextLocationDataResult(data, rect);
} }
bool AXTreeSourceArc::IsScreenReaderMode() const {
return delegate_->IsScreenReaderEnabled();
}
void AXTreeSourceArc::InvalidateTree() { void AXTreeSourceArc::InvalidateTree() {
current_tree_serializer_->Reset(); current_tree_serializer_->Reset();
} }
...@@ -219,6 +223,16 @@ bool AXTreeSourceArc::IsRootOfNodeTree(int32_t id) const { ...@@ -219,6 +223,16 @@ bool AXTreeSourceArc::IsRootOfNodeTree(int32_t id) const {
return !parent_tree_it->second->IsNode(); return !parent_tree_it->second->IsNode();
} }
AccessibilityInfoDataWrapper* AXTreeSourceArc::GetFirstImportantAncestor(
AccessibilityInfoDataWrapper* info_data) const {
AccessibilityInfoDataWrapper* parent = GetParent(info_data);
while (parent && parent->IsNode() &&
!IsImportantInAndroid(parent->GetNode())) {
parent = GetParent(parent);
}
return parent;
}
bool AXTreeSourceArc::GetTreeData(ui::AXTreeData* data) const { bool AXTreeSourceArc::GetTreeData(ui::AXTreeData* data) const {
data->tree_id = ax_tree_id(); data->tree_id = ax_tree_id();
if (android_focused_id_.has_value()) if (android_focused_id_.has_value())
...@@ -332,6 +346,7 @@ void AXTreeSourceArc::BuildImportanceTable( ...@@ -332,6 +346,7 @@ void AXTreeSourceArc::BuildImportanceTable(
AXEventData* event_data, AXEventData* event_data,
const std::map<int32_t, int32_t>& node_id_to_nodes_index, const std::map<int32_t, int32_t>& node_id_to_nodes_index,
std::vector<bool>* out_values) const { std::vector<bool>* out_values) const {
// TODO(hirokisato): move this logic into AccessibilityNodeInfoDataWrapper.
DCHECK(out_values); DCHECK(out_values);
DCHECK(out_values->size() == event_data->node_data.size()); DCHECK(out_values->size() == event_data->node_data.size());
......
...@@ -42,6 +42,7 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*, ...@@ -42,6 +42,7 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*,
class Delegate { class Delegate {
public: public:
virtual void OnAction(const ui::AXActionData& data) const = 0; virtual void OnAction(const ui::AXActionData& data) const = 0;
virtual bool IsScreenReaderEnabled() const = 0;
}; };
AXTreeSourceArc(Delegate* delegate, float device_scale_factor); AXTreeSourceArc(Delegate* delegate, float device_scale_factor);
...@@ -60,10 +61,18 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*, ...@@ -60,10 +61,18 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*,
// Invalidates the tree serializer. // Invalidates the tree serializer.
void InvalidateTree(); void InvalidateTree();
// When it is enabled, this class exposes an accessibility tree optimized for
// screen readers such as ChromeVox and SwitchAccess. This intends to have the
// navigation order and focusabilities similar to TalkBack.
bool IsScreenReaderMode() const;
// Returns true if the node id is the root of the node tree (which can have a // Returns true if the node id is the root of the node tree (which can have a
// parent window). // parent window).
bool IsRootOfNodeTree(int32_t id) const; bool IsRootOfNodeTree(int32_t id) const;
AccessibilityInfoDataWrapper* GetFirstImportantAncestor(
AccessibilityInfoDataWrapper* info_data) const;
// AXTreeSource: // AXTreeSource:
bool GetTreeData(ui::AXTreeData* data) const override; bool GetTreeData(ui::AXTreeData* data) const override;
AccessibilityInfoDataWrapper* GetRoot() const override; AccessibilityInfoDataWrapper* GetRoot() const override;
...@@ -101,6 +110,8 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*, ...@@ -101,6 +110,8 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*,
gfx::Rect* computed_bounds) const; gfx::Rect* computed_bounds) const;
// Computes if the node is clickable and has no clickable descendants. // Computes if the node is clickable and has no clickable descendants.
// TODO(hirokisato): Remove this so that we can handle the case where a parent
// node is clickable and a child node is focusable but not clickable.
bool ComputeIsClickableLeaf( bool ComputeIsClickableLeaf(
const std::vector<mojom::AccessibilityNodeInfoDataPtr>& nodes, const std::vector<mojom::AccessibilityNodeInfoDataPtr>& nodes,
int32_t node_index, int32_t node_index,
......
...@@ -214,12 +214,18 @@ class AXTreeSourceArcTest : public testing::Test, ...@@ -214,12 +214,18 @@ class AXTreeSourceArcTest : public testing::Test,
EXPECT_EQ(expected, tree_text.substr(first_new_line)); EXPECT_EQ(expected, tree_text.substr(first_new_line));
} }
void SetScreenReaderMode(bool enabled) { screen_reader_enabled_ = enabled; }
bool IsScreenReaderEnabled() const override { return screen_reader_enabled_; }
private: private:
void OnAction(const ui::AXActionData& data) const override {} void OnAction(const ui::AXActionData& data) const override {}
const std::unique_ptr<MockAutomationEventRouter> router_; const std::unique_ptr<MockAutomationEventRouter> router_;
const std::unique_ptr<AXTreeSourceArc> tree_source_; const std::unique_ptr<AXTreeSourceArc> tree_source_;
bool screen_reader_enabled_ = false;
DISALLOW_COPY_AND_ASSIGN(AXTreeSourceArcTest); DISALLOW_COPY_AND_ASSIGN(AXTreeSourceArcTest);
}; };
...@@ -384,18 +390,6 @@ TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) { ...@@ -384,18 +390,6 @@ TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) {
AXNodeInfoData* root = event->node_data.back().get(); AXNodeInfoData* root = event->node_data.back().get();
root->id = 10; root->id = 10;
SetProperty(root, AXStringProperty::CLASS_NAME, ""); SetProperty(root, AXStringProperty::CLASS_NAME, "");
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({1, 2}));
// Add child node.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* child1 = event->node_data.back().get();
child1->id = 1;
// Add another child.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* child2 = event->node_data.back().get();
child2->id = 2;
// Populate the tree source with the data. // Populate the tree source with the data.
CallNotifyAccessibilityEvent(event.get()); CallNotifyAccessibilityEvent(event.get());
...@@ -412,7 +406,7 @@ TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) { ...@@ -412,7 +406,7 @@ TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) {
CallNotifyAccessibilityEvent(event.get()); CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(root->id); data = GetSerializedNode(root->id);
// With crrev/1786363, empty text on node will not set the name. // With crrev.com/c/1786363, empty text on node will not set the name.
ASSERT_FALSE( ASSERT_FALSE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
...@@ -453,24 +447,102 @@ TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) { ...@@ -453,24 +447,102 @@ TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) {
ASSERT_TRUE( ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
EXPECT_EQ("label content description label text", name); EXPECT_EQ("label content description label text", name);
}
TEST_F(AXTreeSourceArcTest, AccessibleNameComputationFromDescendants) {
auto event = AXEventData::New();
event->source_id = 0;
event->task_id = 1;
event->event_type = AXEventType::VIEW_FOCUSED;
// Name from contents. event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
AXWindowInfoData* root_window = event->window_data->back().get();
root_window->window_id = 100;
root_window->root_node_id = 10;
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* root = event->node_data.back().get();
root->id = 10;
SetProperty(root, AXStringProperty::CLASS_NAME, "");
SetProperty(root, AXBooleanProperty::IMPORTANCE, true);
SetProperty(root, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({1, 2}));
// Add child node.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* child1 = event->node_data.back().get();
child1->id = 1;
SetProperty(child1, AXBooleanProperty::IMPORTANCE, true);
// Add another child.
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* child2 = event->node_data.back().get();
child2->id = 2;
SetProperty(child2, AXBooleanProperty::IMPORTANCE, true);
// Root node has no name, but has descendants with name. // Root node has no name, but has descendants with name.
root->string_properties->clear(); // Name from contents can happen if a node is focusable.
// Name from contents only happens if a node is clickable. SetProperty(root, AXBooleanProperty::FOCUSABLE, true);
SetProperty(root, AXBooleanProperty::CLICKABLE, true);
SetProperty(child1, AXStringProperty::TEXT, "child1 label text"); SetProperty(child1, AXStringProperty::TEXT, "child1 label text");
SetProperty(child2, AXStringProperty::TEXT, "child2 label text"); SetProperty(child2, AXStringProperty::TEXT, "child2 label text");
// If the screen reader mode is off, do not compute from descendants.
SetScreenReaderMode(false);
CallNotifyAccessibilityEvent(event.get());
ui::AXNodeData data;
data = GetSerializedNode(root->id);
std::string name;
ASSERT_FALSE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
data = GetSerializedNode(child1->id);
ASSERT_FALSE(data.IsIgnored());
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("child1 label text", name);
data = GetSerializedNode(child2->id);
ASSERT_FALSE(data.IsIgnored());
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("child2 label text", name);
// Enable screen reader.
// Compute the name of the clickable node from descendants, and ignore them.
SetScreenReaderMode(true);
CallNotifyAccessibilityEvent(event.get()); CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(root->id); data = GetSerializedNode(root->id);
ASSERT_TRUE( ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("child1 label text child2 label text", name); ASSERT_EQ("child1 label text child2 label text", name);
// If a child is also clickable, do not use child property. data = GetSerializedNode(child1->id);
ASSERT_TRUE(data.IsIgnored());
data = GetSerializedNode(child2->id);
ASSERT_TRUE(data.IsIgnored());
// If one child is clickable, do not use clickable child.
SetProperty(child1, AXBooleanProperty::CLICKABLE, true); SetProperty(child1, AXBooleanProperty::CLICKABLE, true);
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(root->id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("child2 label text", name);
data = GetSerializedNode(child1->id);
ASSERT_FALSE(data.IsIgnored());
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("child1 label text", name);
data = GetSerializedNode(child2->id);
ASSERT_TRUE(data.IsIgnored());
// If both children are also clickable, do not use child properties.
SetProperty(child2, AXBooleanProperty::CLICKABLE, true); SetProperty(child2, AXBooleanProperty::CLICKABLE, true);
CallNotifyAccessibilityEvent(event.get()); CallNotifyAccessibilityEvent(event.get());
...@@ -489,15 +561,6 @@ TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) { ...@@ -489,15 +561,6 @@ TEST_F(AXTreeSourceArcTest, AccessibleNameComputation) {
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name)); data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("root label text", name); ASSERT_EQ("root label text", name);
// The placeholder text on the node, should also be appended to the name.
SetProperty(child2, AXStringProperty::HINT_TEXT, "child2 hint text");
CallNotifyAccessibilityEvent(event.get());
data = GetSerializedNode(child2->id);
ASSERT_TRUE(
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name));
ASSERT_EQ("child2 label text child2 hint text", name);
// Clearing both clickable and name from root, the name should not be // Clearing both clickable and name from root, the name should not be
// populated. // populated.
root->boolean_properties->clear(); root->boolean_properties->clear();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment