Skip to content

Commit 8f3f89f

Browse files
authored
Add framework-side hitTestBehavior support to Semantics (flutter#178817)
This is a reland of flutter#177570, which was reverted in flutter#178744 due to test failures. The original PR introduced `hitTestBehavior` to the semantics framework but incorrectly applied `opaque` behavior to `ModalRoute`, which blocked platform views from receiving pointer events. Instead of making the entire modal opaque, we: 1. Keep `ModalRoute` without explicit `hitTestBehavior` (defaults to `defer`) 2. Make only the dialog/sheet content opaque (blocks clicks to barrier) 3. Platform views remain clickable because they're outside the opaque content boundary Fixes flutter#149001 Original PR: flutter#177570 Revert: flutter#178744
1 parent 882c29a commit 8f3f89f

File tree

14 files changed

+407
-88
lines changed

14 files changed

+407
-88
lines changed

packages/flutter/lib/src/material/bottom_sheet.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
library;
77

88
import 'dart:math' as math;
9+
import 'dart:ui' show SemanticsHitTestBehavior;
910

1011
import 'package:flutter/foundation.dart';
1112
import 'package:flutter/gestures.dart';
@@ -1117,10 +1118,13 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
11171118
),
11181119
);
11191120

1120-
final Widget bottomSheet = useSafeArea
1121+
Widget bottomSheet = useSafeArea
11211122
? SafeArea(bottom: false, child: content)
11221123
: MediaQuery.removePadding(context: context, removeTop: true, child: content);
11231124

1125+
// Prevent clicks inside the bottom sheet from passing through to the barrier
1126+
bottomSheet = Semantics(hitTestBehavior: SemanticsHitTestBehavior.opaque, child: bottomSheet);
1127+
11241128
return capturedThemes?.wrap(bottomSheet) ?? bottomSheet;
11251129
}
11261130

packages/flutter/lib/src/material/dialog.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
/// @docImport 'text_button.dart';
1111
library;
1212

13-
import 'dart:ui' show SemanticsRole, clampDouble, lerpDouble;
13+
import 'dart:ui' show SemanticsHitTestBehavior, SemanticsRole, clampDouble, lerpDouble;
1414

1515
import 'package:flutter/cupertino.dart';
1616
import 'package:flutter/foundation.dart';
@@ -1676,6 +1676,8 @@ class DialogRoute<T> extends RawDialogRoute<T> {
16761676
if (useSafeArea) {
16771677
dialog = SafeArea(child: dialog);
16781678
}
1679+
// Prevent clicks inside the dialog from passing through to the barrier
1680+
dialog = Semantics(hitTestBehavior: SemanticsHitTestBehavior.opaque, child: dialog);
16791681
return dialog;
16801682
},
16811683
barrierLabel: barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel,

packages/flutter/lib/src/rendering/custom_paint.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,9 @@ class RenderCustomPaint extends RenderProxyBox {
10461046
if (config.validationResult != properties.validationResult) {
10471047
config.validationResult = properties.validationResult;
10481048
}
1049+
if (properties.hitTestBehavior != null) {
1050+
config.hitTestBehavior = properties.hitTestBehavior!;
1051+
}
10491052
if (properties.inputType != null) {
10501053
config.inputType = properties.inputType!;
10511054
}

packages/flutter/lib/src/rendering/object.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4940,6 +4940,10 @@ mixin SemanticsAnnotationsMixin on RenderObject {
49404940
config.validationResult = _properties.validationResult;
49414941
}
49424942

4943+
if (_properties.hitTestBehavior != null) {
4944+
config.hitTestBehavior = _properties.hitTestBehavior!;
4945+
}
4946+
49434947
if (_properties.inputType != null) {
49444948
config.inputType = _properties.inputType!;
49454949
}

packages/flutter/lib/src/rendering/platform_view.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
/// @docImport 'package:flutter/widgets.dart';
66
library;
77

8+
import 'dart:ui' as ui show SemanticsHitTestBehavior;
9+
810
import 'package:flutter/foundation.dart';
911
import 'package:flutter/gestures.dart';
1012
import 'package:flutter/scheduler.dart';
@@ -267,6 +269,9 @@ class RenderAndroidView extends PlatformViewRenderBox {
267269

268270
if (_viewController.isCreated) {
269271
config.platformViewId = _viewController.viewId;
272+
// Platform views should allow pointer events to pass through to the
273+
// underlying platform view content.
274+
config.hitTestBehavior = ui.SemanticsHitTestBehavior.transparent;
270275
}
271276
}
272277
}
@@ -372,6 +377,9 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController>
372377
super.describeSemanticsConfiguration(config);
373378
config.isSemanticBoundary = true;
374379
config.platformViewId = _viewController.id;
380+
// Platform views should allow pointer events to pass through to the
381+
// underlying platform view content.
382+
config.hitTestBehavior = ui.SemanticsHitTestBehavior.transparent;
375383
}
376384

377385
@override
@@ -737,6 +745,9 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
737745
super.describeSemanticsConfiguration(config);
738746
config.isSemanticBoundary = true;
739747
config.platformViewId = _controller.viewId;
748+
// Platform views should allow pointer events to pass through to the
749+
// underlying platform view content.
750+
config.hitTestBehavior = ui.SemanticsHitTestBehavior.transparent;
740751
}
741752
}
742753

packages/flutter/lib/src/semantics/semantics.dart

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import 'dart:ui'
2828
StringAttribute,
2929
TextDirection,
3030
Tristate;
31+
import 'dart:ui' as ui show SemanticsHitTestBehavior;
3132

3233
import 'package:collection/collection.dart';
3334
import 'package:flutter/foundation.dart';
@@ -1020,6 +1021,7 @@ class SemanticsData with Diagnosticable {
10201021
required this.role,
10211022
required this.controlsNodes,
10221023
required this.validationResult,
1024+
required this.hitTestBehavior,
10231025
required this.inputType,
10241026
required this.locale,
10251027
this.tags,
@@ -1288,6 +1290,9 @@ class SemanticsData with Diagnosticable {
12881290
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
12891291
final SemanticsValidationResult validationResult;
12901292

1293+
/// {@macro flutter.semantics.SemanticsProperties.hitTestBehavior}
1294+
final ui.SemanticsHitTestBehavior hitTestBehavior;
1295+
12911296
/// {@macro flutter.semantics.SemanticsNode.inputType}
12921297
final SemanticsInputType inputType;
12931298

@@ -1415,6 +1420,7 @@ class SemanticsData with Diagnosticable {
14151420
other.role == role &&
14161421
other.validationResult == validationResult &&
14171422
other.inputType == inputType &&
1423+
other.hitTestBehavior == hitTestBehavior &&
14181424
_sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds) &&
14191425
setEquals<String>(controlsNodes, other.controlsNodes);
14201426
}
@@ -1451,8 +1457,7 @@ class SemanticsData with Diagnosticable {
14511457
validationResult,
14521458
controlsNodes == null ? null : Object.hashAll(controlsNodes!),
14531459
inputType,
1454-
traversalParentIdentifier,
1455-
traversalChildIdentifier,
1460+
hitTestBehavior,
14561461
),
14571462
);
14581463

@@ -1611,6 +1616,7 @@ class SemanticsProperties extends DiagnosticableTree {
16111616
this.controlsNodes,
16121617
this.inputType,
16131618
this.validationResult = SemanticsValidationResult.none,
1619+
this.hitTestBehavior,
16141620
this.onTap,
16151621
this.onLongPress,
16161622
this.onScrollLeft,
@@ -2555,6 +2561,13 @@ class SemanticsProperties extends DiagnosticableTree {
25552561
/// {@endtemplate}
25562562
final SemanticsValidationResult validationResult;
25572563

2564+
/// {@template flutter.semantics.SemanticsProperties.hitTestBehavior}
2565+
/// Describes how the semantic node should behave during hit testing.
2566+
///
2567+
/// See [ui.SemanticsHitTestBehavior] for more details.
2568+
/// {@endtemplate}
2569+
final ui.SemanticsHitTestBehavior? hitTestBehavior;
2570+
25582571
/// {@template flutter.semantics.SemanticsProperties.inputType}
25592572
/// The input type for of a editable widget.
25602573
///
@@ -3235,7 +3248,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
32353248
_headingLevel != config._headingLevel ||
32363249
_linkUrl != config._linkUrl ||
32373250
_role != config.role ||
3238-
_validationResult != config.validationResult;
3251+
_validationResult != config.validationResult ||
3252+
_hitTestBehavior != config.hitTestBehavior;
32393253
}
32403254

32413255
// TAGS, LABELS, ACTIONS
@@ -3529,6 +3543,10 @@ class SemanticsNode with DiagnosticableTreeMixin {
35293543
SemanticsValidationResult get validationResult => _validationResult;
35303544
SemanticsValidationResult _validationResult = _kEmptyConfig.validationResult;
35313545

3546+
/// {@macro flutter.semantics.SemanticsProperties.hitTestBehavior}
3547+
ui.SemanticsHitTestBehavior get hitTestBehavior => _hitTestBehavior;
3548+
ui.SemanticsHitTestBehavior _hitTestBehavior = ui.SemanticsHitTestBehavior.defer;
3549+
35323550
/// {@template flutter.semantics.SemanticsNode.inputType}
35333551
/// The input type for of a editable node.
35343552
///
@@ -3609,6 +3627,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
36093627
_role = config._role;
36103628
_controlsNodes = config._controlsNodes;
36113629
_validationResult = config._validationResult;
3630+
_hitTestBehavior = config._hitTestBehavior;
36123631
_inputType = config._inputType;
36133632
_locale = config.locale;
36143633

@@ -3663,6 +3682,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
36633682
SemanticsRole role = _role;
36643683
Set<String>? controlsNodes = _controlsNodes;
36653684
SemanticsValidationResult validationResult = _validationResult;
3685+
ui.SemanticsHitTestBehavior hitTestBehavior = _hitTestBehavior;
36663686
SemanticsInputType inputType = _inputType;
36673687
final Locale? locale = _locale;
36683688
final Set<int> customSemanticsActionIds = <int>{};
@@ -3727,6 +3747,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
37273747
if (inputType == SemanticsInputType.none) {
37283748
inputType = node._inputType;
37293749
}
3750+
if (hitTestBehavior == ui.SemanticsHitTestBehavior.defer) {
3751+
hitTestBehavior = node._hitTestBehavior;
3752+
}
37303753
if (tooltip == '') {
37313754
tooltip = node._tooltip;
37323755
}
@@ -3818,6 +3841,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
38183841
role: role,
38193842
controlsNodes: controlsNodes,
38203843
validationResult: validationResult,
3844+
hitTestBehavior: hitTestBehavior,
38213845
inputType: inputType,
38223846
locale: locale,
38233847
);
@@ -3989,6 +4013,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
39894013
role: data.role,
39904014
controlsNodes: data.controlsNodes?.toList(),
39914015
validationResult: data.validationResult,
4016+
hitTestBehavior: data.hitTestBehavior,
39924017
inputType: data.inputType,
39934018
locale: data.locale,
39944019
);
@@ -6415,6 +6440,14 @@ class SemanticsConfiguration {
64156440
_hasBeenAnnotated = true;
64166441
}
64176442

6443+
/// {@macro flutter.semantics.SemanticsProperties.hitTestBehavior}
6444+
ui.SemanticsHitTestBehavior get hitTestBehavior => _hitTestBehavior;
6445+
ui.SemanticsHitTestBehavior _hitTestBehavior = ui.SemanticsHitTestBehavior.defer;
6446+
set hitTestBehavior(ui.SemanticsHitTestBehavior value) {
6447+
_hitTestBehavior = value;
6448+
_hasBeenAnnotated = true;
6449+
}
6450+
64186451
/// {@macro flutter.semantics.SemanticsProperties.inputType}
64196452
SemanticsInputType get inputType => _inputType;
64206453
SemanticsInputType _inputType = SemanticsInputType.none;
@@ -6523,6 +6556,10 @@ class SemanticsConfiguration {
65236556
if (_hasExplicitRole && other._hasExplicitRole) {
65246557
return false;
65256558
}
6559+
if (_hitTestBehavior != ui.SemanticsHitTestBehavior.defer ||
6560+
other._hitTestBehavior != ui.SemanticsHitTestBehavior.defer) {
6561+
return false;
6562+
}
65266563
return true;
65276564
}
65286565

@@ -6633,6 +6670,11 @@ class SemanticsConfiguration {
66336670
child._accessiblityFocusBlockType,
66346671
);
66356672

6673+
if (_hitTestBehavior == ui.SemanticsHitTestBehavior.defer &&
6674+
child._hitTestBehavior != ui.SemanticsHitTestBehavior.defer) {
6675+
_hitTestBehavior = child._hitTestBehavior;
6676+
}
6677+
66366678
_hasBeenAnnotated = hasBeenAnnotated || child.hasBeenAnnotated;
66376679
}
66386680

@@ -6678,7 +6720,8 @@ class SemanticsConfiguration {
66786720
.._role = _role
66796721
.._controlsNodes = _controlsNodes
66806722
.._validationResult = _validationResult
6681-
.._inputType = _inputType;
6723+
.._inputType = _inputType
6724+
.._hitTestBehavior = _hitTestBehavior;
66826725
}
66836726
}
66846727

packages/flutter/lib/src/widgets/basic.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
library;
1010

1111
import 'dart:math' as math;
12-
import 'dart:ui' as ui show Image, ImageFilter, SemanticsInputType, TextHeightBehavior;
12+
import 'dart:ui'
13+
as ui
14+
show Image, ImageFilter, SemanticsHitTestBehavior, SemanticsInputType, TextHeightBehavior;
1315

1416
import 'package:flutter/animation.dart';
1517
import 'package:flutter/foundation.dart';
@@ -4048,6 +4050,7 @@ sealed class _SemanticsBase extends SingleChildRenderObjectWidget {
40484050
required SemanticsRole? role,
40494051
required Set<String>? controlsNodes,
40504052
required SemanticsValidationResult validationResult,
4053+
required ui.SemanticsHitTestBehavior? hitTestBehavior,
40514054
required ui.SemanticsInputType? inputType,
40524055
required Locale? localeForSubtree,
40534056
}) : this.fromProperties(
@@ -4133,6 +4136,7 @@ sealed class _SemanticsBase extends SingleChildRenderObjectWidget {
41334136
role: role,
41344137
controlsNodes: controlsNodes,
41354138
validationResult: validationResult,
4139+
hitTestBehavior: hitTestBehavior,
41364140
inputType: inputType,
41374141
),
41384142
);
@@ -4382,6 +4386,7 @@ class SliverSemantics extends _SemanticsBase {
43824386
super.role,
43834387
super.controlsNodes,
43844388
super.validationResult = SemanticsValidationResult.none,
4389+
super.hitTestBehavior,
43854390
super.inputType,
43864391
super.localeForSubtree,
43874392
}) : super(child: sliver);
@@ -7964,6 +7969,7 @@ class Semantics extends _SemanticsBase {
79647969
super.role,
79657970
super.controlsNodes,
79667971
super.validationResult = SemanticsValidationResult.none,
7972+
super.hitTestBehavior,
79677973
super.inputType,
79687974
super.localeForSubtree,
79697975
});

packages/flutter/lib/src/widgets/routes.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
23132313
// To be sorted before the _modalBarrier.
23142314
return _modalScopeCache ??= Semantics(
23152315
sortKey: const OrdinalSortKey(0.0),
2316+
hitTestBehavior: ui.SemanticsHitTestBehavior.opaque,
23162317
child: _ModalScope<T>(
23172318
key: _scopeKey,
23182319
route: this,
@@ -2616,6 +2617,7 @@ class RawDialogRoute<T> extends PopupRoute<T> {
26162617
return Semantics(
26172618
scopesRoute: true,
26182619
explicitChildNodes: true,
2620+
hitTestBehavior: ui.SemanticsHitTestBehavior.opaque,
26192621
child: DisplayFeatureSubScreen(
26202622
anchorPoint: anchorPoint,
26212623
child: _pageBuilder(context, animation, secondaryAnimation),

0 commit comments

Comments
 (0)