Never treat the focused element as invisible or ignored for accessibility.

AXPlatformNodeBase::IsInvisibleOrIgnored is used to determine if, and
how an element should be exposed to platform assistive technologies.
It was not taking into account if the element in question was focused.
As a result, if an author set aria-hidden to "true" on an ancestor of
a focused widget, assistive technologies might not be able to obtain
the selected text or receive the expected notifications when the text
or caret offset changes.

Because it is considered an authoring error to "hide" the focused
element from assistive technologies, modify IsInvisibleOrIgnored to
return false when the node is focused.

Ax-Relnotes: Improves accessibility of focused widgets which have been
incorrectly marked as hidden due to authoring error.

Bug: 1144707
Change-Id: Id97053c1cddf3fa91518aa9950f4bb8ba2ca003a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2521570
Commit-Queue: Joanmarie Diggs <jdiggs@igalia.com>
Reviewed-by: Dominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: Aaron Leventhal <aleventhal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#827262}
diff --git a/content/browser/accessibility/browser_accessibility.cc b/content/browser/accessibility/browser_accessibility.cc
index 9ba1563..b2aacc2 100644
--- a/content/browser/accessibility/browser_accessibility.cc
+++ b/content/browser/accessibility/browser_accessibility.cc
@@ -1511,6 +1511,17 @@
   return PlatformGetRootOfChildTree() ? false : node()->IsLeaf();
 }
 
+bool BrowserAccessibility::IsFocused() const {
+  return manager()->GetFocus() == this;
+}
+
+bool BrowserAccessibility::IsInvisibleOrIgnored() const {
+  if (IsFocused())
+    return false;
+
+  return node()->IsInvisibleOrIgnored();
+}
+
 bool BrowserAccessibility::IsToplevelBrowserWindow() {
   return false;
 }
@@ -1610,7 +1621,7 @@
   return accessible->GetNativeViewAccessible();
 }
 
-gfx::NativeViewAccessible BrowserAccessibility::GetFocus() {
+gfx::NativeViewAccessible BrowserAccessibility::GetFocus() const {
   BrowserAccessibility* focused = manager()->GetFocus();
   if (!focused)
     return nullptr;
diff --git a/content/browser/accessibility/browser_accessibility.h b/content/browser/accessibility/browser_accessibility.h
index 5693c32..8f1b696 100644
--- a/content/browser/accessibility/browser_accessibility.h
+++ b/content/browser/accessibility/browser_accessibility.h
@@ -424,6 +424,8 @@
   bool IsChildOfLeaf() const override;
   bool IsDescendantOfPlainTextField() const override;
   bool IsLeaf() const override;
+  bool IsFocused() const override;
+  bool IsInvisibleOrIgnored() const override;
   bool IsToplevelBrowserWindow() override;
   gfx::NativeViewAccessible GetClosestPlatformObject() const override;
 
@@ -453,7 +455,7 @@
       ui::AXOffscreenResult* offscreen_result = nullptr) const override;
   gfx::NativeViewAccessible HitTestSync(int physical_pixel_x,
                                         int physical_pixel_y) const override;
-  gfx::NativeViewAccessible GetFocus() override;
+  gfx::NativeViewAccessible GetFocus() const override;
   ui::AXPlatformNode* GetFromNodeID(int32_t id) override;
   ui::AXPlatformNode* GetFromTreeIDAndNodeID(const ui::AXTreeID& ax_tree_id,
                                              int32_t id) override;
diff --git a/content/browser/accessibility/browser_accessibility_android.cc b/content/browser/accessibility/browser_accessibility_android.cc
index 2afef4f..7f1f302 100644
--- a/content/browser/accessibility/browser_accessibility_android.cc
+++ b/content/browser/accessibility/browser_accessibility_android.cc
@@ -236,10 +236,6 @@
   return HasState(ax::mojom::State::kFocusable);
 }
 
-bool BrowserAccessibilityAndroid::IsFocused() const {
-  return manager()->GetFocus() == this;
-}
-
 bool BrowserAccessibilityAndroid::IsFormDescendant() const {
   // Iterate over parents and see if any are a form.
   const BrowserAccessibility* parent = PlatformGetParent();
@@ -304,7 +300,7 @@
 }
 
 bool BrowserAccessibilityAndroid::IsVisibleToUser() const {
-  return !HasState(ax::mojom::State::kInvisible);
+  return !IsInvisibleOrIgnored();
 }
 
 bool BrowserAccessibilityAndroid::IsInterestingOnAndroid() const {
@@ -319,7 +315,7 @@
     return false;
 
   // Mark as uninteresting if it's hidden, even if it is focusable.
-  if (HasState(ax::mojom::State::kInvisible))
+  if (IsInvisibleOrIgnored())
     return false;
 
   // Walk up the ancestry. A non-focusable child of a control is not
@@ -330,7 +326,7 @@
       return false;
 
     if (parent->GetRole() == ax::mojom::Role::kIframe &&
-        parent->GetData().HasState(ax::mojom::State::kInvisible)) {
+        parent->IsInvisibleOrIgnored()) {
       return false;
     }
 
diff --git a/content/browser/accessibility/browser_accessibility_android.h b/content/browser/accessibility/browser_accessibility_android.h
index 8ce57ed..0d2c403 100644
--- a/content/browser/accessibility/browser_accessibility_android.h
+++ b/content/browser/accessibility/browser_accessibility_android.h
@@ -40,7 +40,6 @@
   bool IsEnabled() const;
   bool IsExpanded() const;
   bool IsFocusable() const;
-  bool IsFocused() const;
   bool IsFormDescendant() const;
   bool IsHeading() const;
   bool IsHierarchical() const;
diff --git a/content/browser/accessibility/browser_accessibility_cocoa.mm b/content/browser/accessibility/browser_accessibility_cocoa.mm
index d792b57..a1fccac 100644
--- a/content/browser/accessibility/browser_accessibility_cocoa.mm
+++ b/content/browser/accessibility/browser_accessibility_cocoa.mm
@@ -1571,7 +1571,7 @@
   if (![self instanceActive])
     return YES;
   return [[self role] isEqualToString:NSAccessibilityUnknownRole] ||
-         _owner->HasState(ax::mojom::State::kInvisible);
+         _owner->IsInvisibleOrIgnored();
 }
 
 - (NSString*)invalid {
diff --git a/content/browser/accessibility/browser_accessibility_manager.cc b/content/browser/accessibility/browser_accessibility_manager.cc
index 572b0e9..0e3fa3b 100644
--- a/content/browser/accessibility/browser_accessibility_manager.cc
+++ b/content/browser/accessibility/browser_accessibility_manager.cc
@@ -642,10 +642,16 @@
     active_descendant = focus->manager()->GetFromID(active_descendant_id);
   }
 
+  // When getting the active descendant, we avoid calling IsInvisibleOrIgnored
+  // on the node because IsInvisibleOrIgnored takes the focused object into
+  // account by retrieving it. We already have the focused object, thus there is
+  // no need to re-retrieve it. Furthermore, doing so can lead to an infinite
+  // loop. Therefore just check the AXNodeData.
+
   if (focus->GetRole() == ax::mojom::Role::kPopUpButton) {
     BrowserAccessibility* child = focus->InternalGetFirstChild();
     if (child && child->GetRole() == ax::mojom::Role::kMenuListPopup &&
-        !child->GetData().HasState(ax::mojom::State::kInvisible)) {
+        !child->GetData().IsInvisibleOrIgnored()) {
       // The active descendant is found on the menu list popup, i.e. on the
       // actual list and not on the button that opens it.
       // If there is no active descendant, focus should stay on the button so
@@ -661,8 +667,7 @@
     }
   }
 
-  if (active_descendant &&
-      !active_descendant->GetData().HasState(ax::mojom::State::kInvisible))
+  if (active_descendant && !active_descendant->GetData().IsInvisibleOrIgnored())
     return active_descendant;
 
   return focus;
diff --git a/content/browser/accessibility/dump_accessibility_events_browsertest.cc b/content/browser/accessibility/dump_accessibility_events_browsertest.cc
index 118822d4..7c8ec80f 100644
--- a/content/browser/accessibility/dump_accessibility_events_browsertest.cc
+++ b/content/browser/accessibility/dump_accessibility_events_browsertest.cc
@@ -512,6 +512,11 @@
 }
 
 IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
+                       AccessibilityEventsCaretMoveHiddenInput) {
+  RunEventTest(FILE_PATH_LITERAL("caret-move-hidden-input.html"));
+}
+
+IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest,
                        AccessibilityEventsCheckboxValidity) {
   RunEventTest(FILE_PATH_LITERAL("checkbox-validity.html"));
 }
diff --git a/content/browser/accessibility/dump_accessibility_tree_browsertest.cc b/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
index 6213b9b..8c721f2 100644
--- a/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
+++ b/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
@@ -701,6 +701,16 @@
 }
 
 IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
+                       AccessibilityAriaHiddenFocusedButton) {
+  RunAriaTest(FILE_PATH_LITERAL("aria-hidden-focused-button.html"));
+}
+
+IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
+                       AccessibilityAriaHiddenFocusedInput) {
+  RunAriaTest(FILE_PATH_LITERAL("aria-hidden-focused-input.html"));
+}
+
+IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
                        AccessibilityAriaHiddenLabelledBy) {
   RunAriaTest(FILE_PATH_LITERAL("aria-hidden-labelled-by.html"));
 }
diff --git a/content/browser/accessibility/one_shot_accessibility_tree_search.cc b/content/browser/accessibility/one_shot_accessibility_tree_search.cc
index fad2c35..2904cd8 100644
--- a/content/browser/accessibility/one_shot_accessibility_tree_search.cc
+++ b/content/browser/accessibility/one_shot_accessibility_tree_search.cc
@@ -195,7 +195,7 @@
       return false;
   }
 
-  if (node->HasState(ax::mojom::State::kInvisible))
+  if (node->IsInvisibleOrIgnored())
     return false;  // Programmatically hidden, e.g. aria-hidden or via CSS.
 
   if (onscreen_only_ && node->IsOffscreen())
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-android.txt b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-android.txt
new file mode 100644
index 0000000..50ab73c
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-android.txt
@@ -0,0 +1,3 @@
+android.webkit.WebView focusable scrollable
+++android.widget.Button role_description='button' clickable focusable invisible name='Button 1'
+++android.widget.Button role_description='button' clickable focusable focused name='Button 2'
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-auralinux.txt b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-auralinux.txt
new file mode 100644
index 0000000..43ef8c0
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-auralinux.txt
@@ -0,0 +1,3 @@
+[document web] focusable showing visible
+++[push button] name='Button 1' focusable hidden:true
+++[push button] name='Button 2' focusable focused showing visible
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-blink.txt b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-blink.txt
new file mode 100644
index 0000000..cf8de32c
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-blink.txt
@@ -0,0 +1,10 @@
+rootWebArea
+++genericContainer ignored
+++++genericContainer ignored
+++++++genericContainer ignored invisible
+++++++++button invisible name='Button 1'
+++++++++++staticText ignored invisible name='Button 1'
+++++++++staticText ignored invisible name=' '
+++++++++button invisible name='Button 2'
+++++++++++staticText ignored invisible name='Button 2'
+++++++++staticText ignored invisible
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-mac.txt b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-mac.txt
new file mode 100644
index 0000000..3b46c8c
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-mac.txt
@@ -0,0 +1,2 @@
+AXWebArea
+++AXButton AXTitle='Button 2'
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-uia-win.txt b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-uia-win.txt
new file mode 100644
index 0000000..9d6dd5c
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-uia-win.txt
@@ -0,0 +1,3 @@
+Document
+++Button Name='Button 1' IsControlElement=false
+++Button Name='Button 2'
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-win.txt b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-win.txt
new file mode 100644
index 0000000..af13b13
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-button-expected-win.txt
@@ -0,0 +1,3 @@
+ROLE_SYSTEM_DOCUMENT READONLY FOCUSABLE
+++ROLE_SYSTEM_PUSHBUTTON name='Button 1' INVISIBLE FOCUSABLE
+++ROLE_SYSTEM_PUSHBUTTON name='Button 2' FOCUSABLE
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-button-uia-win.txt b/content/test/data/accessibility/aria/aria-hidden-focused-button-uia-win.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-button-uia-win.txt
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-button.html b/content/test/data/accessibility/aria/aria-hidden-focused-button.html
new file mode 100644
index 0000000..08e2ab2
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-button.html
@@ -0,0 +1,17 @@
+<!--
+@AURALINUX-ALLOW:focus*
+@AURALINUX-ALLOW:hidden*
+@AURALINUX-ALLOW:showing
+@AURALINUX-ALLOW:visible
+-->
+<html>
+<body>
+<div aria-hidden="true">
+  <button id="button1">Button 1</button>
+  <button id="button2">Button 2</button>
+</div>
+<script>
+  document.getElementById("button2").focus();
+</script>
+</body>
+</html>
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-android.txt b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-android.txt
new file mode 100644
index 0000000..84be722
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-android.txt
@@ -0,0 +1,3 @@
+android.webkit.WebView focusable scrollable
+++android.widget.EditText clickable editable_text focusable invisible hint='input 1' input_type=1
+++android.widget.EditText clickable editable_text focusable focused hint='input 2' input_type=1
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-auralinux.txt b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-auralinux.txt
new file mode 100644
index 0000000..093561c
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-auralinux.txt
@@ -0,0 +1,3 @@
+[document web] focusable showing visible
+++[entry] name='input 1' focusable selectable-text hidden:true
+++[entry] name='input 2' focusable focused showing visible selectable-text
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-blink.txt b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-blink.txt
new file mode 100644
index 0000000..c857001
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-blink.txt
@@ -0,0 +1,8 @@
+rootWebArea
+++genericContainer ignored
+++++genericContainer ignored
+++++++genericContainer ignored invisible
+++++++++textField invisible name='input 1'
+++++++++staticText ignored invisible name=' '
+++++++++textField invisible name='input 2'
+++++++++staticText ignored invisible
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-mac.txt b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-mac.txt
new file mode 100644
index 0000000..b5fa869
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-mac.txt
@@ -0,0 +1,2 @@
+AXWebArea
+++AXTextField AXDescription='input 2'
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-uia-win.txt b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-uia-win.txt
new file mode 100644
index 0000000..7a319d3
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-uia-win.txt
@@ -0,0 +1,3 @@
+Document
+++Edit Name='input 1' IsControlElement=false
+++Edit Name='input 2'
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-win.txt b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-win.txt
new file mode 100644
index 0000000..6c9c6695
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-input-expected-win.txt
@@ -0,0 +1,3 @@
+ROLE_SYSTEM_DOCUMENT READONLY FOCUSABLE
+++ROLE_SYSTEM_TEXT name='input 1' INVISIBLE FOCUSABLE
+++ROLE_SYSTEM_TEXT name='input 2' FOCUSABLE
diff --git a/content/test/data/accessibility/aria/aria-hidden-focused-input.html b/content/test/data/accessibility/aria/aria-hidden-focused-input.html
new file mode 100644
index 0000000..ba88880
--- /dev/null
+++ b/content/test/data/accessibility/aria/aria-hidden-focused-input.html
@@ -0,0 +1,17 @@
+<!--
+@AURALINUX-ALLOW:focus*
+@AURALINUX-ALLOW:hidden*
+@AURALINUX-ALLOW:showing
+@AURALINUX-ALLOW:visible
+-->
+<html>
+<body>
+<div aria-hidden="true">
+  <input id="input1" aria-label="input 1">
+  <input id="input2" aria-label="input 2">
+</div>
+<script>
+  document.getElementById("input2").focus();
+</script>
+</body>
+</html>
diff --git a/content/test/data/accessibility/event/caret-move-hidden-input-expected-auralinux.txt b/content/test/data/accessibility/event/caret-move-hidden-input-expected-auralinux.txt
new file mode 100644
index 0000000..92001ca
--- /dev/null
+++ b/content/test/data/accessibility/event/caret-move-hidden-input-expected-auralinux.txt
@@ -0,0 +1,5 @@
+FOCUS-EVENT:FALSE role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+FOCUS-EVENT:TRUE role=ROLE_ENTRY name='search' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
+STATE-CHANGE:FOCUSED:FALSE role=ROLE_DOCUMENT_WEB name='(null)' ENABLED,FOCUSABLE,SENSITIVE,SHOWING,VISIBLE
+STATE-CHANGE:FOCUSED:TRUE role=ROLE_ENTRY name='search' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
+TEXT-CARET-MOVED role=ROLE_ENTRY name='search' EDITABLE,ENABLED,FOCUSABLE,FOCUSED,SENSITIVE,SHOWING,SINGLE-LINE,VISIBLE,SELECTABLE-TEXT
diff --git a/content/test/data/accessibility/event/caret-move-hidden-input-expected-uia-win.txt b/content/test/data/accessibility/event/caret-move-hidden-input-expected-uia-win.txt
new file mode 100644
index 0000000..4ed54c9
--- /dev/null
+++ b/content/test/data/accessibility/event/caret-move-hidden-input-expected-uia-win.txt
@@ -0,0 +1,4 @@
+AutomationFocusChanged on role=textbox, name=search
+AutomationFocusChanged on role=textbox, name=search
+Text_TextSelectionChanged on role=document
+Text_TextSelectionChanged on role=textbox, name=search
diff --git a/content/test/data/accessibility/event/caret-move-hidden-input-expected-win.txt b/content/test/data/accessibility/event/caret-move-hidden-input-expected-win.txt
new file mode 100644
index 0000000..e293d8a
--- /dev/null
+++ b/content/test/data/accessibility/event/caret-move-hidden-input-expected-win.txt
@@ -0,0 +1,4 @@
+EVENT_OBJECT_FOCUS on <input#input> role=ROLE_SYSTEM_TEXT name="search" value="foo" FOCUSED,FOCUSABLE IA2_STATE_EDITABLE,IA2_STATE_SELECTABLE_TEXT,IA2_STATE_SINGLE_LINE
+EVENT_OBJECT_LOCATIONCHANGE role=ROLE_SYSTEM_CARET  window_class=Chrome_WidgetWin_0
+EVENT_OBJECT_SHOW role=ROLE_SYSTEM_CARET  window_class=Chrome_WidgetWin_0
+IA2_EVENT_TEXT_CARET_MOVED on <input#input> role=ROLE_SYSTEM_TEXT name="search" value="foo" FOCUSED,FOCUSABLE IA2_STATE_EDITABLE,IA2_STATE_SELECTABLE_TEXT,IA2_STATE_SINGLE_LINE
diff --git a/content/test/data/accessibility/event/caret-move-hidden-input.html b/content/test/data/accessibility/event/caret-move-hidden-input.html
new file mode 100644
index 0000000..e1e1368
--- /dev/null
+++ b/content/test/data/accessibility/event/caret-move-hidden-input.html
@@ -0,0 +1,10 @@
+<div aria-hidden="true">
+  <input id="input" aria-label="search" value="foo">
+</div>
+<script>
+  function go() {
+    var input = document.getElementById("input");
+    input.focus();
+    input.setSelectionRange(1, 1);
+  }
+</script>
diff --git a/ui/accessibility/ax_node.cc b/ui/accessibility/ax_node.cc
index 6fddfb9..a89a9aa 100644
--- a/ui/accessibility/ax_node.cc
+++ b/ui/accessibility/ax_node.cc
@@ -1173,6 +1173,17 @@
   return data().IsIgnored();
 }
 
+bool AXNode::IsInvisibleOrIgnored() const {
+  if (!data().IsInvisibleOrIgnored())
+    return false;
+
+  return !IsFocusedWithinThisTree();
+}
+
+bool AXNode::IsFocusedWithinThisTree() const {
+  return id() == tree_->data().focus_id;
+}
+
 bool AXNode::IsChildOfLeaf() const {
   const AXNode* ancestor = GetUnignoredParent();
   while (ancestor) {
diff --git a/ui/accessibility/ax_node.h b/ui/accessibility/ax_node.h
index 7c531de..7797137 100644
--- a/ui/accessibility/ax_node.h
+++ b/ui/accessibility/ax_node.h
@@ -22,6 +22,7 @@
 
 class AXTableInfo;
 struct AXLanguageInfo;
+struct AXTreeData;
 
 // One node in an AXTree.
 class AX_EXPORT AXNode final {
@@ -55,6 +56,8 @@
     virtual AXTableInfo* GetTableInfo(const AXNode* table_node) const = 0;
     // See AXTree::GetFromId.
     virtual AXNode* GetFromId(int32_t id) const = 0;
+    // See AXTree::data.
+    virtual const AXTreeData& data() const = 0;
 
     virtual base::Optional<int> GetPosInSet(const AXNode& node) = 0;
     virtual base::Optional<int> GetSetSize(const AXNode& node) = 0;
@@ -425,6 +428,12 @@
   // Returns true if node has ignored state or ignored role.
   bool IsIgnored() const;
 
+  // Returns true if node is invisible or ignored.
+  bool IsInvisibleOrIgnored() const;
+
+  // Returns true if node is focused within this tree.
+  bool IsFocusedWithinThisTree() const;
+
   // 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.
diff --git a/ui/accessibility/ax_tree.cc b/ui/accessibility/ax_tree.cc
index 28e1a7a5..3147dc3 100644
--- a/ui/accessibility/ax_tree.cc
+++ b/ui/accessibility/ax_tree.cc
@@ -683,6 +683,10 @@
   return data().tree_id;
 }
 
+const AXTreeData& AXTree::data() const {
+  return data_;
+}
+
 AXNode* AXTree::GetFromId(int32_t id) const {
   auto iter = id_map_.find(id);
   return iter != id_map_.end() ? iter->second : nullptr;
diff --git a/ui/accessibility/ax_tree.h b/ui/accessibility/ax_tree.h
index ae4e4b2..a2d711f 100644
--- a/ui/accessibility/ax_tree.h
+++ b/ui/accessibility/ax_tree.h
@@ -58,7 +58,7 @@
 
   AXNode* root() const { return root_; }
 
-  const AXTreeData& data() const { return data_; }
+  const AXTreeData& data() const override;
 
   // Destroys the tree and notifies all observers.
   void Destroy();
diff --git a/ui/accessibility/platform/ax_fragment_root_win.cc b/ui/accessibility/platform/ax_fragment_root_win.cc
index ea4544e..3b4a270 100644
--- a/ui/accessibility/platform/ax_fragment_root_win.cc
+++ b/ui/accessibility/platform/ax_fragment_root_win.cc
@@ -379,7 +379,7 @@
   return nullptr;
 }
 
-gfx::NativeViewAccessible AXFragmentRootWin::GetFocus() {
+gfx::NativeViewAccessible AXFragmentRootWin::GetFocus() const {
   AXPlatformNodeDelegate* child_delegate = GetChildNodeDelegate();
   if (child_delegate)
     return child_delegate->GetFocus();
diff --git a/ui/accessibility/platform/ax_fragment_root_win.h b/ui/accessibility/platform/ax_fragment_root_win.h
index f8cb803d..6c8ec09 100644
--- a/ui/accessibility/platform/ax_fragment_root_win.h
+++ b/ui/accessibility/platform/ax_fragment_root_win.h
@@ -63,7 +63,7 @@
   gfx::NativeViewAccessible GetNextSibling() override;
   gfx::NativeViewAccessible GetPreviousSibling() override;
   gfx::NativeViewAccessible HitTestSync(int x, int y) const override;
-  gfx::NativeViewAccessible GetFocus() override;
+  gfx::NativeViewAccessible GetFocus() const override;
   const ui::AXUniqueId& GetUniqueId() const override;
   gfx::AcceleratedWidget GetTargetForNativeAccessibilityEvent() override;
   AXPlatformNode* GetFromTreeIDAndNodeID(const ui::AXTreeID& ax_tree_id,
diff --git a/ui/accessibility/platform/ax_platform_node_auralinux.cc b/ui/accessibility/platform/ax_platform_node_auralinux.cc
index 900fe8d..af0c49e 100644
--- a/ui/accessibility/platform/ax_platform_node_auralinux.cc
+++ b/ui/accessibility/platform/ax_platform_node_auralinux.cc
@@ -3062,7 +3062,7 @@
     atk_state_set_add_state(atk_state_set, ATK_STATE_FOCUSABLE);
   if (data.HasState(ax::mojom::State::kHorizontal))
     atk_state_set_add_state(atk_state_set, ATK_STATE_HORIZONTAL);
-  if (!data.HasState(ax::mojom::State::kInvisible)) {
+  if (!IsInvisibleOrIgnored()) {
     atk_state_set_add_state(atk_state_set, ATK_STATE_VISIBLE);
     if (!delegate_->IsOffscreen() && !is_minimized)
       atk_state_set_add_state(atk_state_set, ATK_STATE_SHOWING);
@@ -3693,7 +3693,7 @@
   // on the select element and not the newly-selected descendant.
   if (AXPlatformNodeBase* parent = FromAtkObject(GetParent())) {
     if (parent->GetData().role == ax::mojom::Role::kMenuListPopup)
-      return !parent->GetData().HasState(ax::mojom::State::kInvisible);
+      return !parent->IsInvisibleOrIgnored();
   }
 
   return false;
diff --git a/ui/accessibility/platform/ax_platform_node_base.cc b/ui/accessibility/platform/ax_platform_node_base.cc
index 6472dae..5ba3d96 100644
--- a/ui/accessibility/platform/ax_platform_node_base.cc
+++ b/ui/accessibility/platform/ax_platform_node_base.cc
@@ -119,7 +119,7 @@
   return *empty_data;
 }
 
-gfx::NativeViewAccessible AXPlatformNodeBase::GetFocus() {
+gfx::NativeViewAccessible AXPlatformNodeBase::GetFocus() const {
   if (delegate_)
     return delegate_->GetFocus();
   return nullptr;
@@ -883,9 +883,6 @@
 
 bool AXPlatformNodeBase::HasCaret(
     const AXTree::Selection* unignored_selection) {
-  if (IsInvisibleOrIgnored())
-    return false;
-
   if (IsPlainTextField() &&
       HasIntAttribute(ax::mojom::IntAttribute::kTextSelStart) &&
       HasIntAttribute(ax::mojom::IntAttribute::kTextSelEnd)) {
@@ -917,7 +914,17 @@
 }
 
 bool AXPlatformNodeBase::IsInvisibleOrIgnored() const {
-  return GetData().IsInvisibleOrIgnored();
+  if (!GetData().IsInvisibleOrIgnored())
+    return false;
+
+  if (GetData().HasState(ax::mojom::State::kFocusable))
+    return !IsFocused();
+
+  return !const_cast<AXPlatformNodeBase*>(this)->HasCaret();
+}
+
+bool AXPlatformNodeBase::IsFocused() const {
+  return delegate_ && FromNativeViewAccessible(delegate_->GetFocus()) == this;
 }
 
 bool AXPlatformNodeBase::IsScrollable() const {
diff --git a/ui/accessibility/platform/ax_platform_node_base.h b/ui/accessibility/platform/ax_platform_node_base.h
index 23d8794..2da228c 100644
--- a/ui/accessibility/platform/ax_platform_node_base.h
+++ b/ui/accessibility/platform/ax_platform_node_base.h
@@ -62,7 +62,7 @@
 
   // These are simple wrappers to our delegate.
   const AXNodeData& GetData() const;
-  gfx::NativeViewAccessible GetFocus();
+  gfx::NativeViewAccessible GetFocus() const;
   gfx::NativeViewAccessible GetParent() const;
   int GetChildCount() const;
   gfx::NativeViewAccessible ChildAtIndex(int index) const;
@@ -237,6 +237,9 @@
   // See AXPlatformNodeDelegate::IsInvisibleOrIgnored().
   bool IsInvisibleOrIgnored() const;
 
+  // Returns true if this node is currently focused.
+  bool IsFocused() const;
+
   // Returns true if this node can be scrolled either in the horizontal or the
   // vertical direction.
   bool IsScrollable() const;
diff --git a/ui/accessibility/platform/ax_platform_node_delegate.h b/ui/accessibility/platform/ax_platform_node_delegate.h
index acbf7f2..6c5b5c4 100644
--- a/ui/accessibility/platform/ax_platform_node_delegate.h
+++ b/ui/accessibility/platform/ax_platform_node_delegate.h
@@ -152,6 +152,12 @@
   // layer.
   virtual bool IsLeaf() const = 0;
 
+  // Returns true if this node is invisible or ignored.
+  virtual bool IsInvisibleOrIgnored() const = 0;
+
+  // Returns true if this node is focused.
+  virtual bool IsFocused() const = 0;
+
   // Returns true if this is a top-level browser window that doesn't have a
   // parent accessible node, or its parent is the application accessible node on
   // platforms that have one.
@@ -259,7 +265,7 @@
   // Return the node within this node's subtree (inclusive) that currently has
   // focus, or return nullptr if this subtree is not connected to the top
   // document through its ancestry chain.
-  virtual gfx::NativeViewAccessible GetFocus() = 0;
+  virtual gfx::NativeViewAccessible GetFocus() const = 0;
 
   // Get whether this node is offscreen.
   virtual bool IsOffscreen() const = 0;
diff --git a/ui/accessibility/platform/ax_platform_node_delegate_base.cc b/ui/accessibility/platform/ax_platform_node_delegate_base.cc
index ba49fe98..4497f81 100644
--- a/ui/accessibility/platform/ax_platform_node_delegate_base.cc
+++ b/ui/accessibility/platform/ax_platform_node_delegate_base.cc
@@ -160,6 +160,14 @@
   return !GetChildCount();
 }
 
+bool AXPlatformNodeDelegateBase::IsFocused() const {
+  return false;
+}
+
+bool AXPlatformNodeDelegateBase::IsInvisibleOrIgnored() const {
+  return false;
+}
+
 bool AXPlatformNodeDelegateBase::IsToplevelBrowserWindow() {
   return false;
 }
@@ -313,7 +321,7 @@
   return nullptr;
 }
 
-gfx::NativeViewAccessible AXPlatformNodeDelegateBase::GetFocus() {
+gfx::NativeViewAccessible AXPlatformNodeDelegateBase::GetFocus() const {
   return nullptr;
 }
 
diff --git a/ui/accessibility/platform/ax_platform_node_delegate_base.h b/ui/accessibility/platform/ax_platform_node_delegate_base.h
index 43459f18..04c4194 100644
--- a/ui/accessibility/platform/ax_platform_node_delegate_base.h
+++ b/ui/accessibility/platform/ax_platform_node_delegate_base.h
@@ -74,6 +74,7 @@
   bool IsChildOfLeaf() const override;
   bool IsDescendantOfPlainTextField() const override;
   bool IsLeaf() const override;
+  bool IsFocused() const override;
   bool IsToplevelBrowserWindow() override;
   gfx::NativeViewAccessible GetClosestPlatformObject() const override;
 
@@ -149,11 +150,14 @@
 
   // Return the node within this node's subtree (inclusive) that currently
   // has focus.
-  gfx::NativeViewAccessible GetFocus() override;
+  gfx::NativeViewAccessible GetFocus() const override;
 
   // Get whether this node is offscreen.
   bool IsOffscreen() const override;
 
+  // Returns true if this node is invisible or ignored.
+  bool IsInvisibleOrIgnored() const override;
+
   // Get whether this node is a minimized window.
   bool IsMinimized() const override;
   bool IsText() const override;
diff --git a/ui/accessibility/platform/ax_platform_node_mac.mm b/ui/accessibility/platform/ax_platform_node_mac.mm
index afd13014..1d77e94 100644
--- a/ui/accessibility/platform/ax_platform_node_mac.mm
+++ b/ui/accessibility/platform/ax_platform_node_mac.mm
@@ -490,7 +490,7 @@
     return YES;
 
   return [[self AXRole] isEqualToString:NSAccessibilityUnknownRole] ||
-         _node->GetData().HasState(ax::mojom::State::kInvisible);
+         _node->IsInvisibleOrIgnored();
 }
 
 - (id)accessibilityHitTest:(NSPoint)point {
@@ -955,7 +955,7 @@
     return NO;
 
   return (![[self AXRole] isEqualToString:NSAccessibilityUnknownRole] &&
-          !_node->GetData().HasState(ax::mojom::State::kInvisible));
+          !_node->IsInvisibleOrIgnored());
 }
 - (BOOL)isAccessibilityEnabled {
   if (!_node)
diff --git a/ui/accessibility/platform/ax_platform_node_win.cc b/ui/accessibility/platform/ax_platform_node_win.cc
index cee747e..69eacad 100644
--- a/ui/accessibility/platform/ax_platform_node_win.cc
+++ b/ui/accessibility/platform/ax_platform_node_win.cc
@@ -7191,8 +7191,8 @@
   }  // end of web-content only case.
 
   const AXNodeData& data = GetData();
-  return !(data.HasState(ax::mojom::State::kInvisible) ||
-           (data.IsIgnored() && !data.HasState(ax::mojom::State::kFocusable)));
+  return !(IsInvisibleOrIgnored() &&
+           !data.HasState(ax::mojom::State::kFocusable));
 }
 
 base::Optional<LONG> AXPlatformNodeWin::ComputeUIALandmarkType() const {
@@ -7476,10 +7476,8 @@
   // TODO(dmazzoni): this should probably check if focus is actually inside
   // the menu bar, but we don't currently track focus inside menu pop-ups,
   // and Chrome only has one menu visible at a time so this works for now.
-  if (data.role == ax::mojom::Role::kMenuBar &&
-      !(data.HasState(ax::mojom::State::kInvisible))) {
+  if (data.role == ax::mojom::Role::kMenuBar && !IsInvisibleOrIgnored())
     msaa_state |= STATE_SYSTEM_FOCUSED;
-  }
 
   // Handle STATE_SYSTEM_LINKED
   if (GetData().role == ax::mojom::Role::kLink)
diff --git a/ui/accessibility/platform/test_ax_node_wrapper.cc b/ui/accessibility/platform/test_ax_node_wrapper.cc
index 45d3274..5b93983 100644
--- a/ui/accessibility/platform/test_ax_node_wrapper.cc
+++ b/ui/accessibility/platform/test_ax_node_wrapper.cc
@@ -301,7 +301,7 @@
                  : nullptr;
 }
 
-gfx::NativeViewAccessible TestAXNodeWrapper::GetFocus() {
+gfx::NativeViewAccessible TestAXNodeWrapper::GetFocus() const {
   auto focused = g_focused_node_in_tree.find(tree_);
   if (focused != g_focused_node_in_tree.end() &&
       focused->second->IsDescendantOf(node_)) {
diff --git a/ui/accessibility/platform/test_ax_node_wrapper.h b/ui/accessibility/platform/test_ax_node_wrapper.h
index 4cd51d5..2e8b7e26 100644
--- a/ui/accessibility/platform/test_ax_node_wrapper.h
+++ b/ui/accessibility/platform/test_ax_node_wrapper.h
@@ -93,7 +93,7 @@
   gfx::NativeViewAccessible HitTestSync(
       int screen_physical_pixel_x,
       int screen_physical_pixel_y) const override;
-  gfx::NativeViewAccessible GetFocus() override;
+  gfx::NativeViewAccessible GetFocus() const override;
   bool IsMinimized() const override;
   bool IsWebContent() const override;
   AXPlatformNode* GetFromNodeID(int32_t id) override;
diff --git a/ui/views/accessibility/ax_virtual_view.cc b/ui/views/accessibility/ax_virtual_view.cc
index cc9f8aa..fdca5c0 100644
--- a/ui/views/accessibility/ax_virtual_view.cc
+++ b/ui/views/accessibility/ax_virtual_view.cc
@@ -373,7 +373,7 @@
   return nullptr;
 }
 
-gfx::NativeViewAccessible AXVirtualView::GetFocus() {
+gfx::NativeViewAccessible AXVirtualView::GetFocus() const {
   View* owner_view = GetOwnerView();
   if (owner_view) {
     if (!(owner_view->HasFocus())) {
diff --git a/ui/views/accessibility/ax_virtual_view.h b/ui/views/accessibility/ax_virtual_view.h
index 299d106a..48b99df 100644
--- a/ui/views/accessibility/ax_virtual_view.h
+++ b/ui/views/accessibility/ax_virtual_view.h
@@ -149,7 +149,7 @@
   gfx::NativeViewAccessible HitTestSync(
       int screen_physical_pixel_x,
       int screen_physical_pixel_y) const override;
-  gfx::NativeViewAccessible GetFocus() override;
+  gfx::NativeViewAccessible GetFocus() const override;
   ui::AXPlatformNode* GetFromNodeID(int32_t id) override;
   bool AccessibilityPerformAction(const ui::AXActionData& data) override;
   bool ShouldIgnoreHoveredStateForTesting() override;
diff --git a/ui/views/accessibility/view_ax_platform_node_delegate.cc b/ui/views/accessibility/view_ax_platform_node_delegate.cc
index 00b72e9..255b360 100644
--- a/ui/views/accessibility/view_ax_platform_node_delegate.cc
+++ b/ui/views/accessibility/view_ax_platform_node_delegate.cc
@@ -381,6 +381,10 @@
   return ViewAccessibility::IsLeaf() || AXPlatformNodeDelegateBase::IsLeaf();
 }
 
+bool ViewAXPlatformNodeDelegate::IsFocused() const {
+  return GetFocus() == GetNativeObject();
+}
+
 bool ViewAXPlatformNodeDelegate::IsToplevelBrowserWindow() {
   // Note: only used on Desktop Linux. Other platforms don't have an application
   // node so this would never return true.
@@ -478,7 +482,7 @@
                                      : (*i)->GetNativeViewAccessible();
 }
 
-gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetFocus() {
+gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::GetFocus() const {
   gfx::NativeViewAccessible focus_override =
       ui::AXPlatformNode::GetPopupFocusOverride();
   if (focus_override)
diff --git a/ui/views/accessibility/view_ax_platform_node_delegate.h b/ui/views/accessibility/view_ax_platform_node_delegate.h
index f4ba9a4..7905742 100644
--- a/ui/views/accessibility/view_ax_platform_node_delegate.h
+++ b/ui/views/accessibility/view_ax_platform_node_delegate.h
@@ -65,6 +65,7 @@
   gfx::NativeViewAccessible GetParent() override;
   bool IsChildOfLeaf() const override;
   bool IsLeaf() const override;
+  bool IsFocused() const override;
   bool IsToplevelBrowserWindow() override;
   gfx::Rect GetBoundsRect(
       const ui::AXCoordinateSystem coordinate_system,
@@ -73,7 +74,7 @@
   gfx::NativeViewAccessible HitTestSync(
       int screen_physical_pixel_x,
       int screen_physical_pixel_y) const override;
-  gfx::NativeViewAccessible GetFocus() override;
+  gfx::NativeViewAccessible GetFocus() const override;
   ui::AXPlatformNode* GetFromNodeID(int32_t id) override;
   ui::AXPlatformNode* GetFromTreeIDAndNodeID(const ui::AXTreeID& ax_tree_id,
                                              int32_t id) override;