Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 206e86e

Browse files
authored
macOS: Update platform node when AXNodeData role changes (#54364)
Fixes flutter/flutter#151428 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making or feature I am adding, or the PR is [test-exempt]. See [testing the engine] for instructions on writing and running engine tests. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I signed the [CLA]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
1 parent 7f06c0f commit 206e86e

9 files changed

+121
-4
lines changed

shell/platform/common/accessibility_bridge.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@ void AccessibilityBridge::OnRoleChanged(ui::AXTree* tree,
163163
ax::mojom::Role old_role,
164164
ax::mojom::Role new_role) {}
165165

166+
void AccessibilityBridge::OnNodeDataChanged(
167+
ui::AXTree* tree,
168+
const ui::AXNodeData& old_node_data,
169+
const ui::AXNodeData& new_node_data) {
170+
auto platform_view =
171+
GetFlutterPlatformNodeDelegateFromID(new_node_data.id).lock();
172+
if (platform_view) {
173+
platform_view->NodeDataChanged(old_node_data, new_node_data);
174+
}
175+
}
176+
166177
void AccessibilityBridge::OnNodeCreated(ui::AXTree* tree, ui::AXNode* node) {
167178
BASE_DCHECK(node);
168179
id_wrapper_map_[node->id()] = CreateFlutterPlatformNodeDelegate();

shell/platform/common/accessibility_bridge.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ class AccessibilityBridge
260260
ax::mojom::Role old_role,
261261
ax::mojom::Role new_role) override;
262262

263+
// |AXTreeObserver|
264+
void OnNodeDataChanged(ui::AXTree* tree,
265+
const ui::AXNodeData& old_node_data,
266+
const ui::AXNodeData& new_node_data) override;
267+
263268
// |AXTreeObserver|
264269
void OnAtomicUpdateFinished(
265270
ui::AXTree* tree,

shell/platform/common/flutter_platform_node_delegate.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ class FlutterPlatformNodeDelegate : public ui::AXPlatformNodeDelegateBase {
139139
/// Subclasses must call super.
140140
virtual void Init(std::weak_ptr<OwnerBridge> bridge, ui::AXNode* node);
141141

142+
//------------------------------------------------------------------------------
143+
// @brief Called when node was updated. Subclasses can override this
144+
// to update platform nodes.
145+
virtual void NodeDataChanged(const ui::AXNodeData& old_node_data,
146+
const ui::AXNodeData& new_node_data) {}
147+
142148
//------------------------------------------------------------------------------
143149
/// @brief Gets the underlying ax node for this platform node delegate.
144150
ui::AXNode* GetAXNode() const;

shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,11 @@
165165
// If it is a text field, the value change notifications are handled by
166166
// the FlutterTextField directly. Only need to make sure it is the
167167
// first responder.
168-
FlutterTextField* native_text_field =
169-
(FlutterTextField*)mac_platform_node_delegate->GetNativeViewAccessible();
168+
id native_text_field = mac_platform_node_delegate->GetNativeViewAccessible();
169+
FML_DCHECK([native_text_field isKindOfClass:FlutterTextField.class]);
170170
id focused = mac_platform_node_delegate->GetFocus();
171171
if (!focused || native_text_field == focused) {
172-
[native_text_field startEditing];
172+
[(FlutterTextField*)native_text_field startEditing];
173173
}
174174
break;
175175
}

shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class FlutterPlatformNodeDelegateMac : public FlutterPlatformNodeDelegate {
2727

2828
void Init(std::weak_ptr<OwnerBridge> bridge, ui::AXNode* node) override;
2929

30+
void NodeDataChanged(const ui::AXNodeData& old_node_data,
31+
const ui::AXNodeData& new_node_data) override;
32+
3033
//---------------------------------------------------------------------------
3134
/// @brief Gets the live region text of this node in UTF-8 format. This
3235
/// is useful to determine the changes in between semantics

shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMac.mm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@
3535
NSCAssert(ax_platform_node_, @"Failed to create platform node.");
3636
}
3737

38+
void FlutterPlatformNodeDelegateMac::NodeDataChanged(const ui::AXNodeData& old_node_data,
39+
const ui::AXNodeData& new_node_data) {
40+
if (old_node_data.IsTextField() && !new_node_data.IsTextField()) {
41+
ax_platform_node_->Destroy();
42+
ax_platform_node_ = ui::AXPlatformNode::Create(this);
43+
} else if (!old_node_data.IsTextField() && new_node_data.IsTextField()) {
44+
ax_platform_node_->Destroy();
45+
ax_platform_node_ = new FlutterTextPlatformNode(this, view_controller_);
46+
}
47+
}
48+
3849
FlutterPlatformNodeDelegateMac::~FlutterPlatformNodeDelegateMac() {
3950
// Destroy() also calls delete on itself.
4051
ax_platform_node_->Destroy();

shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,81 @@
296296
EXPECT_EQ([native_text_field.stringValue isEqualToString:@"textfield"], YES);
297297
}
298298

299+
TEST(FlutterPlatformNodeDelegateMac, ChangingFlagsUpdatesNativeViewAccessible) {
300+
FlutterViewController* viewController = CreateTestViewController();
301+
FlutterEngine* engine = viewController.engine;
302+
[viewController loadView];
303+
304+
// Creates a NSWindow so that the native text field can become first responder.
305+
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
306+
styleMask:NSBorderlessWindowMask
307+
backing:NSBackingStoreBuffered
308+
defer:NO];
309+
window.contentView = viewController.view;
310+
engine.semanticsEnabled = YES;
311+
312+
auto bridge = viewController.accessibilityBridge.lock();
313+
// Initialize ax node data.
314+
FlutterSemanticsNode2 root;
315+
root.id = 0;
316+
root.flags = static_cast<FlutterSemanticsFlag>(0);
317+
root.actions = static_cast<FlutterSemanticsAction>(0);
318+
root.label = "root";
319+
root.hint = "";
320+
root.value = "";
321+
root.increased_value = "";
322+
root.decreased_value = "";
323+
root.tooltip = "";
324+
root.child_count = 1;
325+
int32_t children[] = {1};
326+
root.children_in_traversal_order = children;
327+
root.custom_accessibility_actions_count = 0;
328+
root.rect = {0, 0, 100, 100}; // LTRB
329+
root.transform = {1, 0, 0, 0, 1, 0, 0, 0, 1};
330+
bridge->AddFlutterSemanticsNodeUpdate(root);
331+
332+
double rectSize = 50;
333+
double transformFactor = 0.5;
334+
335+
FlutterSemanticsNode2 child1;
336+
child1.id = 1;
337+
child1.flags = static_cast<FlutterSemanticsFlag>(0);
338+
child1.actions = static_cast<FlutterSemanticsAction>(0);
339+
child1.label = "";
340+
child1.hint = "";
341+
child1.value = "textfield";
342+
child1.increased_value = "";
343+
child1.decreased_value = "";
344+
child1.tooltip = "";
345+
child1.text_selection_base = -1;
346+
child1.text_selection_extent = -1;
347+
child1.child_count = 0;
348+
child1.custom_accessibility_actions_count = 0;
349+
child1.rect = {0, 0, rectSize, rectSize}; // LTRB
350+
child1.transform = {transformFactor, 0, 0, 0, transformFactor, 0, 0, 0, 1};
351+
bridge->AddFlutterSemanticsNodeUpdate(child1);
352+
353+
bridge->CommitUpdates();
354+
355+
auto child_platform_node_delegate = bridge->GetFlutterPlatformNodeDelegateFromID(1).lock();
356+
// Verify the accessibility attribute matches.
357+
id native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
358+
EXPECT_TRUE([[native_accessibility className] isEqualToString:@"AXPlatformNodeCocoa"]);
359+
360+
// Converting child to text field should produce `FlutterTextField` native view accessible.
361+
child1.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField;
362+
bridge->AddFlutterSemanticsNodeUpdate(child1);
363+
bridge->CommitUpdates();
364+
365+
native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
366+
EXPECT_TRUE([native_accessibility isKindOfClass:[FlutterTextField class]]);
367+
368+
child1.flags = static_cast<FlutterSemanticsFlag>(0);
369+
bridge->AddFlutterSemanticsNodeUpdate(child1);
370+
bridge->CommitUpdates();
371+
372+
native_accessibility = child_platform_node_delegate->GetNativeViewAccessible();
373+
EXPECT_TRUE([[native_accessibility className] isEqualToString:@"AXPlatformNodeCocoa"]);
374+
}
375+
299376
} // namespace flutter::testing

shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ - (void)dealloc {
190190
FlutterPlatformNodeDelegate* delegate = static_cast<FlutterPlatformNodeDelegate*>(GetDelegate());
191191
bool offscreen;
192192
auto bridge_ptr = delegate->GetOwnerBridge().lock();
193+
if (!bridge_ptr) {
194+
return NSZeroRect;
195+
}
193196
gfx::RectF bounds = bridge_ptr->RelativeToGlobalBounds(delegate->GetAXNode(), offscreen, true);
194197

195198
// Converts to NSRect to use NSView rect conversion.

shell/platform/darwin/macos/framework/Source/FlutterViewController.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,8 @@ - (BOOL)attached {
519519
}
520520

521521
- (void)updateSemantics:(const FlutterSemanticsUpdate2*)update {
522-
NSAssert(_engine.semanticsEnabled, @"Semantics must be enabled.");
522+
// Semantics will be disabled when unfocusing application but the updateSemantics:
523+
// callback is received in next run loop turn.
523524
if (!_engine.semanticsEnabled) {
524525
return;
525526
}

0 commit comments

Comments
 (0)