mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This is a reland of #178817, which was reverted in #179100 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 #149001 Original PR: #177570 Revert: #178744
This commit is contained in:
parent
e25d71b086
commit
2a950efec4
@ -6,6 +6,7 @@
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show SemanticsHitTestBehavior;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -1117,10 +1118,13 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
|
||||
),
|
||||
);
|
||||
|
||||
final Widget bottomSheet = useSafeArea
|
||||
Widget bottomSheet = useSafeArea
|
||||
? SafeArea(bottom: false, child: content)
|
||||
: MediaQuery.removePadding(context: context, removeTop: true, child: content);
|
||||
|
||||
// Prevent clicks inside the bottom sheet from passing through to the barrier
|
||||
bottomSheet = Semantics(hitTestBehavior: SemanticsHitTestBehavior.opaque, child: bottomSheet);
|
||||
|
||||
return capturedThemes?.wrap(bottomSheet) ?? bottomSheet;
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
/// @docImport 'text_button.dart';
|
||||
library;
|
||||
|
||||
import 'dart:ui' show SemanticsRole, clampDouble, lerpDouble;
|
||||
import 'dart:ui' show SemanticsHitTestBehavior, SemanticsRole, clampDouble, lerpDouble;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -1676,6 +1676,8 @@ class DialogRoute<T> extends RawDialogRoute<T> {
|
||||
if (useSafeArea) {
|
||||
dialog = SafeArea(child: dialog);
|
||||
}
|
||||
// Prevent clicks inside the dialog from passing through to the barrier
|
||||
dialog = Semantics(hitTestBehavior: SemanticsHitTestBehavior.opaque, child: dialog);
|
||||
return dialog;
|
||||
},
|
||||
barrierLabel: barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
|
||||
@ -1043,6 +1043,9 @@ class RenderCustomPaint extends RenderProxyBox {
|
||||
if (config.validationResult != properties.validationResult) {
|
||||
config.validationResult = properties.validationResult;
|
||||
}
|
||||
if (properties.hitTestBehavior != null) {
|
||||
config.hitTestBehavior = properties.hitTestBehavior!;
|
||||
}
|
||||
if (properties.inputType != null) {
|
||||
config.inputType = properties.inputType!;
|
||||
}
|
||||
|
||||
@ -4938,6 +4938,10 @@ mixin SemanticsAnnotationsMixin on RenderObject {
|
||||
config.validationResult = _properties.validationResult;
|
||||
}
|
||||
|
||||
if (_properties.hitTestBehavior != null) {
|
||||
config.hitTestBehavior = _properties.hitTestBehavior!;
|
||||
}
|
||||
|
||||
if (_properties.inputType != null) {
|
||||
config.inputType = _properties.inputType!;
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
/// @docImport 'package:flutter/widgets.dart';
|
||||
library;
|
||||
|
||||
import 'dart:ui' as ui show SemanticsHitTestBehavior;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
@ -267,6 +269,9 @@ class RenderAndroidView extends PlatformViewRenderBox {
|
||||
|
||||
if (_viewController.isCreated) {
|
||||
config.platformViewId = _viewController.viewId;
|
||||
// Platform views should allow pointer events to pass through to the
|
||||
// underlying platform view content.
|
||||
config.hitTestBehavior = ui.SemanticsHitTestBehavior.transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -372,6 +377,9 @@ abstract class RenderDarwinPlatformView<T extends DarwinPlatformViewController>
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config.isSemanticBoundary = true;
|
||||
config.platformViewId = _viewController.id;
|
||||
// Platform views should allow pointer events to pass through to the
|
||||
// underlying platform view content.
|
||||
config.hitTestBehavior = ui.SemanticsHitTestBehavior.transparent;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -737,6 +745,9 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config.isSemanticBoundary = true;
|
||||
config.platformViewId = _controller.viewId;
|
||||
// Platform views should allow pointer events to pass through to the
|
||||
// underlying platform view content.
|
||||
config.hitTestBehavior = ui.SemanticsHitTestBehavior.transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import 'dart:ui'
|
||||
StringAttribute,
|
||||
TextDirection,
|
||||
Tristate;
|
||||
import 'dart:ui' as ui show SemanticsHitTestBehavior;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -1020,6 +1021,7 @@ class SemanticsData with Diagnosticable {
|
||||
required this.role,
|
||||
required this.controlsNodes,
|
||||
required this.validationResult,
|
||||
required this.hitTestBehavior,
|
||||
required this.inputType,
|
||||
required this.locale,
|
||||
this.tags,
|
||||
@ -1288,6 +1290,9 @@ class SemanticsData with Diagnosticable {
|
||||
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
|
||||
final SemanticsValidationResult validationResult;
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.hitTestBehavior}
|
||||
final ui.SemanticsHitTestBehavior hitTestBehavior;
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsNode.inputType}
|
||||
final SemanticsInputType inputType;
|
||||
|
||||
@ -1415,6 +1420,7 @@ class SemanticsData with Diagnosticable {
|
||||
other.role == role &&
|
||||
other.validationResult == validationResult &&
|
||||
other.inputType == inputType &&
|
||||
other.hitTestBehavior == hitTestBehavior &&
|
||||
_sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds) &&
|
||||
setEquals<String>(controlsNodes, other.controlsNodes);
|
||||
}
|
||||
@ -1451,8 +1457,7 @@ class SemanticsData with Diagnosticable {
|
||||
validationResult,
|
||||
controlsNodes == null ? null : Object.hashAll(controlsNodes!),
|
||||
inputType,
|
||||
traversalParentIdentifier,
|
||||
traversalChildIdentifier,
|
||||
hitTestBehavior,
|
||||
),
|
||||
);
|
||||
|
||||
@ -1611,6 +1616,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
this.controlsNodes,
|
||||
this.inputType,
|
||||
this.validationResult = SemanticsValidationResult.none,
|
||||
this.hitTestBehavior,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onScrollLeft,
|
||||
@ -2555,6 +2561,13 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
/// {@endtemplate}
|
||||
final SemanticsValidationResult validationResult;
|
||||
|
||||
/// {@template flutter.semantics.SemanticsProperties.hitTestBehavior}
|
||||
/// Describes how the semantic node should behave during hit testing.
|
||||
///
|
||||
/// See [ui.SemanticsHitTestBehavior] for more details.
|
||||
/// {@endtemplate}
|
||||
final ui.SemanticsHitTestBehavior? hitTestBehavior;
|
||||
|
||||
/// {@template flutter.semantics.SemanticsProperties.inputType}
|
||||
/// The input type for of a editable widget.
|
||||
///
|
||||
@ -3235,7 +3248,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
_headingLevel != config._headingLevel ||
|
||||
_linkUrl != config._linkUrl ||
|
||||
_role != config.role ||
|
||||
_validationResult != config.validationResult;
|
||||
_validationResult != config.validationResult ||
|
||||
_hitTestBehavior != config.hitTestBehavior;
|
||||
}
|
||||
|
||||
// TAGS, LABELS, ACTIONS
|
||||
@ -3529,6 +3543,10 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
SemanticsValidationResult get validationResult => _validationResult;
|
||||
SemanticsValidationResult _validationResult = _kEmptyConfig.validationResult;
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.hitTestBehavior}
|
||||
ui.SemanticsHitTestBehavior get hitTestBehavior => _hitTestBehavior;
|
||||
ui.SemanticsHitTestBehavior _hitTestBehavior = ui.SemanticsHitTestBehavior.defer;
|
||||
|
||||
/// {@template flutter.semantics.SemanticsNode.inputType}
|
||||
/// The input type for of a editable node.
|
||||
///
|
||||
@ -3609,6 +3627,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
_role = config._role;
|
||||
_controlsNodes = config._controlsNodes;
|
||||
_validationResult = config._validationResult;
|
||||
_hitTestBehavior = config._hitTestBehavior;
|
||||
_inputType = config._inputType;
|
||||
_locale = config.locale;
|
||||
|
||||
@ -3663,6 +3682,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
SemanticsRole role = _role;
|
||||
Set<String>? controlsNodes = _controlsNodes;
|
||||
SemanticsValidationResult validationResult = _validationResult;
|
||||
ui.SemanticsHitTestBehavior hitTestBehavior = _hitTestBehavior;
|
||||
SemanticsInputType inputType = _inputType;
|
||||
final Locale? locale = _locale;
|
||||
final customSemanticsActionIds = <int>{};
|
||||
@ -3727,6 +3747,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
if (inputType == SemanticsInputType.none) {
|
||||
inputType = node._inputType;
|
||||
}
|
||||
if (hitTestBehavior == ui.SemanticsHitTestBehavior.defer) {
|
||||
hitTestBehavior = node._hitTestBehavior;
|
||||
}
|
||||
if (tooltip == '') {
|
||||
tooltip = node._tooltip;
|
||||
}
|
||||
@ -3818,6 +3841,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
role: role,
|
||||
controlsNodes: controlsNodes,
|
||||
validationResult: validationResult,
|
||||
hitTestBehavior: hitTestBehavior,
|
||||
inputType: inputType,
|
||||
locale: locale,
|
||||
);
|
||||
@ -3989,6 +4013,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
role: data.role,
|
||||
controlsNodes: data.controlsNodes?.toList(),
|
||||
validationResult: data.validationResult,
|
||||
hitTestBehavior: data.hitTestBehavior,
|
||||
inputType: data.inputType,
|
||||
locale: data.locale,
|
||||
);
|
||||
@ -6412,6 +6437,14 @@ class SemanticsConfiguration {
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// {@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;
|
||||
}
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.inputType}
|
||||
SemanticsInputType get inputType => _inputType;
|
||||
SemanticsInputType _inputType = SemanticsInputType.none;
|
||||
@ -6523,6 +6556,10 @@ class SemanticsConfiguration {
|
||||
if (_hasExplicitRole && other._hasExplicitRole) {
|
||||
return false;
|
||||
}
|
||||
if (_hitTestBehavior != ui.SemanticsHitTestBehavior.defer ||
|
||||
other._hitTestBehavior != ui.SemanticsHitTestBehavior.defer) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -6633,6 +6670,11 @@ class SemanticsConfiguration {
|
||||
child._accessiblityFocusBlockType,
|
||||
);
|
||||
|
||||
if (_hitTestBehavior == ui.SemanticsHitTestBehavior.defer &&
|
||||
child._hitTestBehavior != ui.SemanticsHitTestBehavior.defer) {
|
||||
_hitTestBehavior = child._hitTestBehavior;
|
||||
}
|
||||
|
||||
_hasBeenAnnotated = hasBeenAnnotated || child.hasBeenAnnotated;
|
||||
}
|
||||
|
||||
@ -6678,7 +6720,8 @@ class SemanticsConfiguration {
|
||||
.._role = _role
|
||||
.._controlsNodes = _controlsNodes
|
||||
.._validationResult = _validationResult
|
||||
.._inputType = _inputType;
|
||||
.._inputType = _inputType
|
||||
.._hitTestBehavior = _hitTestBehavior;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui show Image, ImageFilter, SemanticsInputType, TextHeightBehavior;
|
||||
import 'dart:ui'
|
||||
as ui
|
||||
show Image, ImageFilter, SemanticsHitTestBehavior, SemanticsInputType, TextHeightBehavior;
|
||||
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -4047,6 +4049,7 @@ sealed class _SemanticsBase extends SingleChildRenderObjectWidget {
|
||||
required SemanticsRole? role,
|
||||
required Set<String>? controlsNodes,
|
||||
required SemanticsValidationResult validationResult,
|
||||
required ui.SemanticsHitTestBehavior? hitTestBehavior,
|
||||
required ui.SemanticsInputType? inputType,
|
||||
required Locale? localeForSubtree,
|
||||
}) : this.fromProperties(
|
||||
@ -4132,6 +4135,7 @@ sealed class _SemanticsBase extends SingleChildRenderObjectWidget {
|
||||
role: role,
|
||||
controlsNodes: controlsNodes,
|
||||
validationResult: validationResult,
|
||||
hitTestBehavior: hitTestBehavior,
|
||||
inputType: inputType,
|
||||
),
|
||||
);
|
||||
@ -4377,6 +4381,7 @@ class SliverSemantics extends _SemanticsBase {
|
||||
super.role,
|
||||
super.controlsNodes,
|
||||
super.validationResult = SemanticsValidationResult.none,
|
||||
super.hitTestBehavior,
|
||||
super.inputType,
|
||||
super.localeForSubtree,
|
||||
}) : super(child: sliver);
|
||||
@ -7959,6 +7964,7 @@ class Semantics extends _SemanticsBase {
|
||||
super.role,
|
||||
super.controlsNodes,
|
||||
super.validationResult = SemanticsValidationResult.none,
|
||||
super.hitTestBehavior,
|
||||
super.inputType,
|
||||
super.localeForSubtree,
|
||||
});
|
||||
|
||||
@ -893,11 +893,18 @@ void main() {
|
||||
TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'Dialog',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(label: 'BottomSheet', textDirection: TextDirection.ltr),
|
||||
TestSemantics(
|
||||
label: 'Dialog',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.scopesRoute,
|
||||
SemanticsFlag.namesRoute,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(label: 'BottomSheet', textDirection: TextDirection.ltr),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -1069,14 +1076,24 @@ void main() {
|
||||
TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'Dialog',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
label: 'Dialog',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.scopesRoute,
|
||||
SemanticsFlag.namesRoute,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(label: 'BottomSheet', textDirection: TextDirection.ltr),
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'BottomSheet',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -1145,14 +1162,24 @@ void main() {
|
||||
TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'Dialog',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
label: 'Dialog',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.scopesRoute,
|
||||
SemanticsFlag.namesRoute,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(label: 'BottomSheet', textDirection: TextDirection.ltr),
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'BottomSheet',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -1217,17 +1244,24 @@ void main() {
|
||||
TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'Dialog',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isButton],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: 'Dismiss',
|
||||
label: 'Dialog',
|
||||
textDirection: TextDirection.ltr,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.scopesRoute,
|
||||
SemanticsFlag.namesRoute,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isButton],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
label: 'Dismiss',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
TestSemantics(label: 'BottomSheet', textDirection: TextDirection.ltr),
|
||||
],
|
||||
),
|
||||
TestSemantics(label: 'BottomSheet', textDirection: TextDirection.ltr),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -2958,6 +2992,61 @@ void main() {
|
||||
// Test with theme.platform = iOS on different real platforms.
|
||||
await pumpModalBottomSheetWithTheme(TargetPlatform.iOS);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
testWidgets('Modal bottom sheet has hitTestBehavior.opaque to prevent dismissal on empty areas', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final semantics = SemanticsTester(tester);
|
||||
late BuildContext savedContext;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (BuildContext context) {
|
||||
savedContext = context;
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: savedContext,
|
||||
builder: (BuildContext context) => Container(
|
||||
height: 200,
|
||||
color: Colors.blue,
|
||||
child: const Center(child: Text('Modal Bottom Sheet')),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Modal Bottom Sheet'), findsOneWidget);
|
||||
|
||||
// Verify the route-level Semantics has opaque hitTestBehavior
|
||||
// This prevents clicks inside the bottom sheet from passing through to the barrier
|
||||
final List<Semantics> allSemantics = tester
|
||||
.widgetList<Semantics>(
|
||||
find.ancestor(of: find.text('Modal Bottom Sheet'), matching: find.byType(Semantics)),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final Semantics routeSemantics = allSemantics.firstWhere(
|
||||
(Semantics s) => s.properties.hitTestBehavior == SemanticsHitTestBehavior.opaque,
|
||||
);
|
||||
|
||||
expect(routeSemantics.properties.hitTestBehavior, SemanticsHitTestBehavior.opaque);
|
||||
|
||||
final Semantics widgetSemantics = allSemantics.firstWhere(
|
||||
(Semantics s) => s.properties.scopesRoute ?? false,
|
||||
);
|
||||
|
||||
expect(widgetSemantics.properties.scopesRoute, true);
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
class _TestPage extends StatelessWidget {
|
||||
|
||||
@ -3311,6 +3311,95 @@ void main() {
|
||||
semantics.dispose();
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
});
|
||||
|
||||
testWidgets('Dialog has hitTestBehavior.opaque to prevent dismissal on empty areas', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final semantics = SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
const Dialog(child: SizedBox(width: 200, height: 200)),
|
||||
);
|
||||
},
|
||||
child: const Text('Show Dialog'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Show Dialog'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(Dialog), findsOneWidget);
|
||||
|
||||
final Semantics routeSemantics = tester.widget<Semantics>(
|
||||
find.ancestor(of: find.byType(Dialog), matching: find.byType(Semantics)).first,
|
||||
);
|
||||
|
||||
expect(routeSemantics.properties.hitTestBehavior, SemanticsHitTestBehavior.opaque);
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('AlertDialog has hitTestBehavior.opaque via Dialog', (WidgetTester tester) async {
|
||||
final semantics = SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: const Text('Test Dialog'),
|
||||
content: const SizedBox(width: 200, height: 100),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Show Dialog'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Show Dialog'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
expect(find.byType(Dialog), findsOneWidget);
|
||||
|
||||
// Find the route-level Semantics with hitTestBehavior.opaque
|
||||
// (wraps the entire dialog content, above the Dialog widget)
|
||||
final Semantics routeSemantics = tester.widget<Semantics>(
|
||||
find.ancestor(of: find.byType(Dialog), matching: find.byType(Semantics)).first,
|
||||
);
|
||||
|
||||
expect(routeSemantics.properties.hitTestBehavior, SemanticsHitTestBehavior.opaque);
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
|
||||
@ -1460,6 +1460,7 @@ void main() {
|
||||
role: SemanticsRole.menu,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute],
|
||||
label: 'Popup menu',
|
||||
textDirection: TextDirection.ltr,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
|
||||
@ -557,6 +557,22 @@ void main() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('PlatformViewRenderBox has transparent hitTestBehavior in semantics', () {
|
||||
final controller = FakePlatformViewController(0);
|
||||
final renderBox = PlatformViewRenderBox(
|
||||
controller: controller,
|
||||
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
|
||||
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{},
|
||||
);
|
||||
|
||||
final config = SemanticsConfiguration();
|
||||
renderBox.describeSemanticsConfiguration(config);
|
||||
|
||||
expect(config.hitTestBehavior, ui.SemanticsHitTestBehavior.transparent);
|
||||
expect(config.isSemanticBoundary, true);
|
||||
expect(config.platformViewId, 0);
|
||||
});
|
||||
}
|
||||
|
||||
ui.PointerData _pointerData(
|
||||
|
||||
@ -1035,6 +1035,34 @@ void main() {
|
||||
expect(config.customSemanticsActions[customAction], same(onCustomAction));
|
||||
});
|
||||
|
||||
test('SemanticsConfiguration.copy() preserves hitTestBehavior', () {
|
||||
final config = SemanticsConfiguration()
|
||||
..isSemanticBoundary = true
|
||||
..label = 'test'
|
||||
..hitTestBehavior = SemanticsHitTestBehavior.opaque;
|
||||
|
||||
expect(config.hitTestBehavior, SemanticsHitTestBehavior.opaque);
|
||||
|
||||
final SemanticsConfiguration copy = config.copy();
|
||||
|
||||
expect(copy.hitTestBehavior, SemanticsHitTestBehavior.opaque);
|
||||
expect(copy.isSemanticBoundary, isTrue);
|
||||
expect(copy.label, 'test');
|
||||
});
|
||||
|
||||
test('SemanticsConfiguration.copy() preserves all hitTestBehavior values', () {
|
||||
final deferConfig = SemanticsConfiguration();
|
||||
expect(deferConfig.copy().hitTestBehavior, SemanticsHitTestBehavior.defer);
|
||||
|
||||
final opaqueConfig = SemanticsConfiguration()
|
||||
..hitTestBehavior = SemanticsHitTestBehavior.opaque;
|
||||
expect(opaqueConfig.copy().hitTestBehavior, SemanticsHitTestBehavior.opaque);
|
||||
|
||||
final transparentConfig = SemanticsConfiguration()
|
||||
..hitTestBehavior = SemanticsHitTestBehavior.transparent;
|
||||
expect(transparentConfig.copy().hitTestBehavior, SemanticsHitTestBehavior.transparent);
|
||||
});
|
||||
|
||||
test('SemanticsOwner dispatches memory events', () async {
|
||||
await expectLater(
|
||||
await memoryEvents(
|
||||
|
||||
@ -753,6 +753,7 @@ void main() {
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
||||
inputType: ui.SemanticsInputType.none,
|
||||
locale: null,
|
||||
);
|
||||
@ -1055,6 +1056,7 @@ void main() {
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
||||
inputType: ui.SemanticsInputType.none,
|
||||
locale: null,
|
||||
);
|
||||
@ -1157,6 +1159,7 @@ void main() {
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
||||
inputType: ui.SemanticsInputType.none,
|
||||
locale: null,
|
||||
);
|
||||
@ -1264,6 +1267,7 @@ void main() {
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
||||
inputType: ui.SemanticsInputType.none,
|
||||
locale: null,
|
||||
);
|
||||
@ -1299,6 +1303,7 @@ void main() {
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
||||
inputType: ui.SemanticsInputType.none,
|
||||
locale: null,
|
||||
);
|
||||
@ -1433,6 +1438,7 @@ void main() {
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
hitTestBehavior: ui.SemanticsHitTestBehavior.defer,
|
||||
inputType: ui.SemanticsInputType.none,
|
||||
locale: null,
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user