Add framework-side hitTestBehavior support for Semantics widget and apply to ModalRoute#177570
Add framework-side hitTestBehavior support for Semantics widget and apply to ModalRoute#177570flutter-zl merged 13 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces hitTestBehavior support to the semantics layer, enabling finer control over how semantic nodes behave during hit testing. The new property is consistently plumbed through SemanticsData, SemanticsProperties, SemanticsNode, and SemanticsConfiguration. A key application of this is in ModalRoute, where hitTestBehavior is set to opaque to correctly block semantics of routes underneath, which is a great improvement for accessibility. The implementation is solid, but I've identified a few areas in packages/flutter/lib/src/semantics/semantics.dart where the new property needs to be handled in related methods like equality checks, copying, and merging to prevent potential bugs.
| /// {@macro flutter.semantics.SemanticsProperties.hitTestBehavior} | ||
| final ui.SemanticsHitTestBehavior hitTestBehavior; |
| SemanticsRole role = _role; | ||
| Set<String>? controlsNodes = _controlsNodes; | ||
| SemanticsValidationResult validationResult = _validationResult; | ||
| ui.SemanticsHitTestBehavior hitTestBehavior = _hitTestBehavior; |
There was a problem hiding this comment.
In getSemanticsData, when merging descendants, the hitTestBehavior from child nodes is not being merged. This could lead to incorrect semantics. You should add logic to merge hitTestBehavior inside the _visitDescendants loop (around line 3524), similar to how role and inputType are handled. For example:
if (hitTestBehavior == ui.SemanticsHitTestBehavior.defer) {
hitTestBehavior = node.hitTestBehavior;
}| /// {@macro flutter.semantics.SemanticsProperties.hitTestBehavior} | ||
| ui.SemanticsHitTestBehavior get hitTestBehavior => _hitTestBehavior; | ||
| ui.SemanticsHitTestBehavior _hitTestBehavior = ui.SemanticsHitTestBehavior.defer; | ||
| set hitTestBehavior(ui.SemanticsHitTestBehavior value) { | ||
| _hitTestBehavior = value; | ||
| _hasBeenAnnotated = true; | ||
| } |
There was a problem hiding this comment.
With the addition of the hitTestBehavior property, a few related methods in SemanticsConfiguration need to be updated:
- The
copy()method (around line 6184) is missing.._hitTestBehavior = _hitTestBehavior;. Without this, the property will be lost when a configuration is copied. - The
absorb()method (around line 6093) does not handle merging_hitTestBehaviorfrom the child configuration. You might want to merge it if the current configuration has the default value. - The
isCompatibleWith()method (around line 6054) should probably check for conflictinghitTestBehaviorvalues to prevent merging incompatible configurations.
These omissions could lead to incorrect semantics behavior.
There was a problem hiding this comment.
isCompatibleWith is not updated?
There was a problem hiding this comment.
Good idea. Updated.
| } | ||
|
|
||
| if (_hitTestBehavior == ui.SemanticsHitTestBehavior.defer && | ||
| child._hitTestBehavior != ui.SemanticsHitTestBehavior.defer) { |
There was a problem hiding this comment.
The second check is not needed
There was a problem hiding this comment.
Good idea. Updated.
| /// {@macro flutter.semantics.SemanticsProperties.hitTestBehavior} | ||
| ui.SemanticsHitTestBehavior get hitTestBehavior => _hitTestBehavior; | ||
| ui.SemanticsHitTestBehavior _hitTestBehavior = ui.SemanticsHitTestBehavior.defer; | ||
| set hitTestBehavior(ui.SemanticsHitTestBehavior value) { | ||
| _hitTestBehavior = value; | ||
| _hasBeenAnnotated = true; | ||
| } |
There was a problem hiding this comment.
isCompatibleWith is not updated?
| role: role, | ||
| controlsNodes: controlsNodes, | ||
| validationResult: validationResult, | ||
| hitTestBehavior: hitTestBehavior, |
There was a problem hiding this comment.
we may need to force node that sets this to not merge up
For example
Column(
children: [
Text('child1'),
Semantics(hitTestBehavior: .opaque, child: Text('child2')),
Text('child3'),
]
)If you print the semantics tree you will see the entire column will form a node, and the opaque will merge up to apply to the entire node
There are two way to fix this
- force Semantics widget that sets hitTestBehavior to also have container:true
- implement SemanticsConfiguration.isCompatible to reject the merge in this case
I think going with (2) makes more sense
There was a problem hiding this comment.
Good idea. Updated.
4a0f085 to
597edea
Compare
437e060 to
0aa125a
Compare
…et and apply to ModalRoute (flutter/flutter#177570)
…et and apply to ModalRoute (flutter/flutter#177570)
…et and apply to ModalRoute (flutter/flutter#177570)
|
Reason for revert: Broke internal tests. |
…et and apply to ModalRoute (flutter/flutter#177570)
…get and apply to ModalRoute (#177570)" (#178744) <!-- start_original_pr_link --> Reverts: #177570 <!-- end_original_pr_link --> <!-- start_initiating_author --> Initiated by: chingjun <!-- end_initiating_author --> <!-- start_revert_reason --> Reason for reverting: Broke internal tests. <!-- end_revert_reason --> <!-- start_original_pr_author --> Original PR Author: flutter-zl <!-- end_original_pr_author --> <!-- start_reviewers --> Reviewed By: {chunhtai} <!-- end_reviewers --> <!-- start_revert_body --> This change reverts the following previous change: Fix premature dialog dismissal on Flutter Web when semantics are enabled by correctly propagating hitTestBehavior through the semantics pipeline and declaring modal routes as opaque to pointer events. Before change https://dialog-dismiss-before.web.app/ Click on the "Show Dialog" button. Click anywhere inside the dialog that is not a form field. Observe the dialog being dismissed. After change https://dialog-dimiss-after.web.app/ Click on the "Show Dialog" button. Click anywhere inside the dialog that is not a form field. Observe the dialog not dismissed. Fixes: #149001 Engine work: #176974 <!-- end_revert_body --> Co-authored-by: auto-submit[bot] <[email protected]>
flutter/flutter@cc14ef5...cb7b7df 2025-11-18 [email protected] Enable UIScene Migration and update create templates (flutter/flutter#178700) 2025-11-18 Minsuk Jung Fix #160622: change containsWatchConpanion function to detect companion watch apps defined by only the project info file. (flutter/flutter#176832) 2025-11-18 [email protected] Roll Skia from 614e71550fc3 to ca906091e199 (2 revisions) (flutter/flutter#178716) 2025-11-18 [email protected] Revert "[ Tool ] Don't delete `.dart_tool/widget_preview_scaffold` during `flutter clean` (#175664)" (flutter/flutter#178672) 2025-11-18 [email protected] Add missing flutter_lints dev dependencies (flutter/flutter#178105) 2025-11-18 [email protected] Roll Skia from ec2f626cdcad to 614e71550fc3 (3 revisions) (flutter/flutter#178708) 2025-11-18 [email protected] Roll Dart SDK from a8ad764281e3 to 312845b06afc (1 revision) (flutter/flutter#178704) 2025-11-18 [email protected] Roll Skia from d7268f8245f2 to ec2f626cdcad (1 revision) (flutter/flutter#178703) 2025-11-18 [email protected] Refactor SnackBar behavior selection example to use `RadioGroup` (flutter/flutter#178618) 2025-11-18 [email protected] Add framework-side hitTestBehavior support for Semantics widget and apply to ModalRoute (flutter/flutter#177570) 2025-11-18 [email protected] Fix deprecation warning in some API examples using RadioListTile (flutter/flutter#178635) 2025-11-18 [email protected] Roll Skia from 47fd0d9b1044 to d7268f8245f2 (6 revisions) (flutter/flutter#178695) 2025-11-18 [email protected] Roll Dart SDK from cf94632d94a1 to a8ad764281e3 (1 revision) (flutter/flutter#178694) 2025-11-18 [email protected] [fuchsia] Add wrapper for zx_iob_writev (flutter/flutter#178626) 2025-11-17 [email protected] Make a11y `computeChildGeometry` slightly faster (flutter/flutter#177477) 2025-11-17 [email protected] Fix DropdownMenu width when decorationBuilder provides label (flutter/flutter#178465) 2025-11-17 [email protected] Add DropdownMenuFormField.decorationBuilder (flutter/flutter#178640) 2025-11-17 [email protected] Roll Skia from 84c83c0dfb4a to 47fd0d9b1044 (4 revisions) (flutter/flutter#178673) 2025-11-17 [email protected] Small cleanup in `AndroidTouchProcessor.java` (flutter/flutter#178574) 2025-11-17 [email protected] Remove unnecessary `final` modifier in `StandardMessageCodec.java` (flutter/flutter#178598) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Fix compilation errors by adding required hitTestBehavior parameter to all 6 SemanticsData constructor calls in matchers_test.dart. This was missing from the original PR flutter#177570 changes.
Fix premature dialog dismissal on Flutter Web when semantics are enabled by correctly propagating hitTestBehavior through the semantics pipeline and declaring modal routes as opaque to pointer events.
Before change
https://dialog-dismiss-before.web.app/
Click on the "Show Dialog" button.
Click anywhere inside the dialog that is not a form field.
Observe the dialog being dismissed.
After change
https://dialog-dimiss-after.web.app/
Click on the "Show Dialog" button.
Click anywhere inside the dialog that is not a form field.
Observe the dialog not dismissed.
Fixes: #149001
Engine work: #176974