diff --git a/shell/platform/common/accessibility_bridge.cc b/shell/platform/common/accessibility_bridge.cc index 9a6e02065ee64..1913a580c509e 100644 --- a/shell/platform/common/accessibility_bridge.cc +++ b/shell/platform/common/accessibility_bridge.cc @@ -163,6 +163,17 @@ void AccessibilityBridge::OnRoleChanged(ui::AXTree* tree, ax::mojom::Role old_role, ax::mojom::Role new_role) {} +void AccessibilityBridge::OnNodeDataChanged( + ui::AXTree* tree, + const ui::AXNodeData& old_node_data, + const ui::AXNodeData& new_node_data) { + auto platform_view = + GetFlutterPlatformNodeDelegateFromID(new_node_data.id).lock(); + if (platform_view) { + platform_view->NodeDataChanged(old_node_data, new_node_data); + } +} + void AccessibilityBridge::OnNodeCreated(ui::AXTree* tree, ui::AXNode* node) { BASE_DCHECK(node); id_wrapper_map_[node->id()] = CreateFlutterPlatformNodeDelegate(); diff --git a/shell/platform/common/accessibility_bridge.h b/shell/platform/common/accessibility_bridge.h index 7e6698021b726..8126c1ce7b65a 100644 --- a/shell/platform/common/accessibility_bridge.h +++ b/shell/platform/common/accessibility_bridge.h @@ -260,6 +260,11 @@ class AccessibilityBridge ax::mojom::Role old_role, ax::mojom::Role new_role) override; + // |AXTreeObserver| + void OnNodeDataChanged(ui::AXTree* tree, + const ui::AXNodeData& old_node_data, + const ui::AXNodeData& new_node_data) override; + // |AXTreeObserver| void OnAtomicUpdateFinished( ui::AXTree* tree, diff --git a/shell/platform/common/flutter_platform_node_delegate.h b/shell/platform/common/flutter_platform_node_delegate.h index 6172f4f86b9f2..9c90d5a953fd3 100644 --- a/shell/platform/common/flutter_platform_node_delegate.h +++ b/shell/platform/common/flutter_platform_node_delegate.h @@ -139,6 +139,12 @@ class FlutterPlatformNodeDelegate : public ui::AXPlatformNodeDelegateBase { /// Subclasses must call super. virtual void Init(std::weak_ptr bridge, ui::AXNode* node); + //------------------------------------------------------------------------------ + // @brief Called when node was updated. Subclasses can override this + // to update platform nodes. + virtual void NodeDataChanged(const ui::AXNodeData& old_node_data, + const ui::AXNodeData& new_node_data) {} + //------------------------------------------------------------------------------ /// @brief Gets the underlying ax node for this platform node delegate. ui::AXNode* GetAXNode() const; diff --git a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm index fc624b8a9ee32..f79f60b10e67e 100644 --- a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm +++ b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm @@ -165,11 +165,11 @@ // If it is a text field, the value change notifications are handled by // the FlutterTextField directly. Only need to make sure it is the // first responder. - FlutterTextField* native_text_field = - (FlutterTextField*)mac_platform_node_delegate->GetNativeViewAccessible(); + id native_text_field = mac_platform_node_delegate->GetNativeViewAccessible(); + FML_DCHECK([native_text_field isKindOfClass:FlutterTextField.class]); id focused = mac_platform_node_delegate->GetFocus(); if (!focused || native_text_field == focused) { - [native_text_field startEditing]; + [(FlutterTextField*)native_text_field startEditing]; } break; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h index 71fb34e4627ba..62014f670ddf1 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h @@ -27,6 +27,9 @@ class FlutterPlatformNodeDelegateMac : public FlutterPlatformNodeDelegate { void Init(std::weak_ptr bridge, ui::AXNode* node) override; + void NodeDataChanged(const ui::AXNodeData& old_node_data, + const ui::AXNodeData& new_node_data) override; + //--------------------------------------------------------------------------- /// @brief Gets the live region text of this node in UTF-8 format. This /// is useful to determine the changes in between semantics diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm index 1828f94d92807..5a6c0235d0931 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm @@ -35,6 +35,17 @@ NSCAssert(ax_platform_node_, @"Failed to create platform node."); } +void FlutterPlatformNodeDelegateMac::NodeDataChanged(const ui::AXNodeData& old_node_data, + const ui::AXNodeData& new_node_data) { + if (old_node_data.IsTextField() && !new_node_data.IsTextField()) { + ax_platform_node_->Destroy(); + ax_platform_node_ = ui::AXPlatformNode::Create(this); + } else if (!old_node_data.IsTextField() && new_node_data.IsTextField()) { + ax_platform_node_->Destroy(); + ax_platform_node_ = new FlutterTextPlatformNode(this, view_controller_); + } +} + FlutterPlatformNodeDelegateMac::~FlutterPlatformNodeDelegateMac() { // Destroy() also calls delete on itself. ax_platform_node_->Destroy(); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm index d7c12689fe729..ceb4d1bdf0b04 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm @@ -296,4 +296,81 @@ EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES); } +TEST(FlutterPlatformNodeDelegateMac, ChangingFlagsUpdatesNativeViewAccessible) { + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; + [viewController loadView]; + + // Creates a NSWindow so that the native text field can become first responder. + NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO]; + window.contentView = viewController.view; + engine.semanticsEnabled = YES; + + auto bridge = viewController.accessibilityBridge.lock(); + // Initialize ax node data. + FlutterSemanticsNode2 root; + root.id = 0; + root.flags = static_cast(0); + root.actions = static_cast(0); + root.label = "root"; + root.hint = ""; + root.value = ""; + root.increased_value = ""; + root.decreased_value = ""; + root.tooltip = ""; + root.child_count = 1; + int32_t children[] = {1}; + root.children_in_traversal_order = children; + root.custom_accessibility_actions_count = 0; + root.rect = {0, 0, 100, 100}; // LTRB + root.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + bridge->AddFlutterSemanticsNodeUpdate(root); + + double rectSize = 50; + double transformFactor = 0.5; + + FlutterSemanticsNode2 child1; + child1.id = 1; + child1.flags = static_cast(0); + child1.actions = static_cast(0); + child1.label = ""; + child1.hint = ""; + child1.value = "textfield"; + child1.increased_value = ""; + child1.decreased_value = ""; + child1.tooltip = ""; + child1.text_selection_base = -1; + child1.text_selection_extent = -1; + child1.child_count = 0; + child1.custom_accessibility_actions_count = 0; + child1.rect = {0, 0, rectSize, rectSize}; // LTRB + child1.transform = {transformFactor, 0, 0, 0, transformFactor, 0, 0, 0, 1}; + bridge->AddFlutterSemanticsNodeUpdate(child1); + + bridge->CommitUpdates(); + + auto child_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock(); + // Verify the accessibility attribute matches. + id native_accessibility = child_platform_node_delegate->GetNativeViewAccessible(); + EXPECT_TRUE([[native_accessibility className] isEqualToString:@"AXPlatformNodeCocoa"]); + + // Converting child to text field should produce `FlutterTextField` native view accessible. + child1.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField; + bridge->AddFlutterSemanticsNodeUpdate(child1); + bridge->CommitUpdates(); + + native_accessibility = child_platform_node_delegate->GetNativeViewAccessible(); + EXPECT_TRUE([native_accessibility isKindOfClass:[FlutterTextField class]]); + + child1.flags = static_cast(0); + bridge->AddFlutterSemanticsNodeUpdate(child1); + bridge->CommitUpdates(); + + native_accessibility = child_platform_node_delegate->GetNativeViewAccessible(); + EXPECT_TRUE([[native_accessibility className] isEqualToString:@"AXPlatformNodeCocoa"]); +} + } // namespace flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm index 7aa874bbc905c..db9947574e50c 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm @@ -190,6 +190,9 @@ - (void)dealloc { FlutterPlatformNodeDelegate* delegate = static_cast(GetDelegate()); bool offscreen; auto bridge_ptr = delegate->GetOwnerBridge().lock(); + if (!bridge_ptr) { + return NSZeroRect; + } gfx::RectF bounds = bridge_ptr->RelativeToGlobalBounds(delegate->GetAXNode(), offscreen, true); // Converts to NSRect to use NSView rect conversion. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index a091a48a56945..98a10e734ae83 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -519,7 +519,8 @@ - (BOOL)attached { } - (void)updateSemantics:(const FlutterSemanticsUpdate2*)update { - NSAssert(_engine.semanticsEnabled, @"Semantics must be enabled."); + // Semantics will be disabled when unfocusing application but the updateSemantics: + // callback is received in next run loop turn. if (!_engine.semanticsEnabled) { return; }