ModalBarrier and Drawer barrier prevents mouse events (#44296)
* Add opaque to barriers * Detect opaque and test
This commit is contained in:
@@ -546,9 +546,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
|
||||
onTap: close,
|
||||
child: Semantics(
|
||||
label: MaterialLocalizations.of(context)?.modalBarrierDismissLabel,
|
||||
child: Container( // The drawer's "scrim"
|
||||
color: _scrimColorTween.evaluate(_controller),
|
||||
),
|
||||
child: MouseRegion(
|
||||
opaque: true,
|
||||
child: Container( // The drawer's "scrim"
|
||||
color: _scrimColorTween.evaluate(_controller),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2615,12 +2615,13 @@ class RenderMouseRegion extends RenderProxyBox {
|
||||
PointerEnterEventListener onEnter,
|
||||
PointerHoverEventListener onHover,
|
||||
PointerExitEventListener onExit,
|
||||
this.opaque = true,
|
||||
bool opaque = true,
|
||||
RenderBox child,
|
||||
}) : assert(opaque != null),
|
||||
_onEnter = onEnter,
|
||||
_onHover = onHover,
|
||||
_onExit = onExit,
|
||||
_opaque = opaque,
|
||||
_annotationIsActive = false,
|
||||
super(child) {
|
||||
_hoverAnnotation = MouseTrackerAnnotation(
|
||||
@@ -2644,7 +2645,14 @@ class RenderMouseRegion extends RenderProxyBox {
|
||||
/// pointer is within their areas.
|
||||
///
|
||||
/// This defaults to true.
|
||||
bool opaque;
|
||||
bool get opaque => _opaque;
|
||||
bool _opaque;
|
||||
set opaque(bool value) {
|
||||
if (_opaque != value) {
|
||||
_opaque = value;
|
||||
_updateAnnotations();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when a mouse pointer enters the region (with or without buttons
|
||||
/// pressed).
|
||||
@@ -2705,7 +2713,8 @@ class RenderMouseRegion extends RenderProxyBox {
|
||||
final bool annotationWillBeActive = (
|
||||
_onEnter != null ||
|
||||
_onHover != null ||
|
||||
_onExit != null
|
||||
_onExit != null ||
|
||||
opaque
|
||||
) &&
|
||||
RendererBinding.instance.mouseTracker.mouseIsConnected;
|
||||
if (annotationWasActive != annotationWillBeActive) {
|
||||
|
||||
@@ -101,11 +101,14 @@ class ModalBarrier extends StatelessWidget {
|
||||
child: Semantics(
|
||||
label: semanticsDismissible ? semanticsLabel : null,
|
||||
textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: color == null ? null : DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
child: MouseRegion(
|
||||
opaque: true,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: color == null ? null : DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -78,6 +78,78 @@ void main() {
|
||||
expect(find.text('drawer'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Drawer hover test', (WidgetTester tester) async {
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final List<String> logs = <String>[];
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
|
||||
// Start out of hoverTarget
|
||||
await gesture.moveTo(const Offset(100, 100));
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
key: scaffoldKey,
|
||||
drawer: const Text('drawer'),
|
||||
body: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) { logs.add('enter'); },
|
||||
onHover: (_) { logs.add('hover'); },
|
||||
onExit: (_) { logs.add('exit'); },
|
||||
child: Container(width: 10, height: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(logs, isEmpty);
|
||||
expect(find.text('drawer'), findsNothing);
|
||||
|
||||
// When drawer is closed, hover is interactable
|
||||
await gesture.moveTo(const Offset(5, 5));
|
||||
await tester.pump(); // no effect
|
||||
expect(logs, <String>['enter', 'hover']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.moveTo(const Offset(20, 20));
|
||||
await tester.pump(); // no effect
|
||||
expect(logs, <String>['exit']);
|
||||
logs.clear();
|
||||
|
||||
// When drawer is open, hover is uninteractable
|
||||
scaffoldKey.currentState.openDrawer();
|
||||
await tester.pump(const Duration(seconds: 1)); // animation done
|
||||
expect(find.text('drawer'), findsOneWidget);
|
||||
|
||||
await gesture.moveTo(const Offset(5, 5));
|
||||
await tester.pump(); // no effect
|
||||
expect(logs, isEmpty);
|
||||
logs.clear();
|
||||
|
||||
await gesture.moveTo(const Offset(20, 20));
|
||||
await tester.pump(); // no effect
|
||||
expect(logs, isEmpty);
|
||||
logs.clear();
|
||||
|
||||
// Close drawer, hover is interactable again
|
||||
await tester.tapAt(const Offset(750.0, 100.0)); // on the mask
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1)); // animation done
|
||||
expect(find.text('drawer'), findsNothing);
|
||||
|
||||
await gesture.moveTo(const Offset(5, 5));
|
||||
await tester.pump(); // no effect
|
||||
expect(logs, <String>['enter', 'hover']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.moveTo(const Offset(20, 20));
|
||||
await tester.pump(); // no effect
|
||||
expect(logs, <String>['exit']);
|
||||
logs.clear();
|
||||
});
|
||||
|
||||
testWidgets('Drawer drag cancel resume (LTR)', (WidgetTester tester) async {
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
await tester.pumpWidget(
|
||||
@@ -324,5 +396,3 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,13 +7,15 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/gestures.dart' show kSecondaryButton;
|
||||
import 'package:flutter/gestures.dart' show kSecondaryButton, PointerDeviceKind;
|
||||
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
bool tapped;
|
||||
bool hovered;
|
||||
Widget tapTarget;
|
||||
Widget hoverTarget;
|
||||
|
||||
setUp(() {
|
||||
tapped = false;
|
||||
@@ -27,6 +29,18 @@ void main() {
|
||||
child: Text('target', textDirection: TextDirection.ltr),
|
||||
),
|
||||
);
|
||||
|
||||
hovered = false;
|
||||
hoverTarget = MouseRegion(
|
||||
onHover: (_) { hovered = true; },
|
||||
onEnter: (_) { hovered = true; },
|
||||
onExit: (_) { hovered = true; },
|
||||
child: const SizedBox(
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
child: Text('target', textDirection: TextDirection.ltr),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('ModalBarrier prevents interactions with widgets behind it', (WidgetTester tester) async {
|
||||
@@ -45,6 +59,35 @@ void main() {
|
||||
reason: 'because the tap is not prevented by ModalBarrier');
|
||||
});
|
||||
|
||||
testWidgets('ModalBarrier prevents hover interactions with widgets behind it', (WidgetTester tester) async {
|
||||
final Widget subject = Stack(
|
||||
textDirection: TextDirection.ltr,
|
||||
children: <Widget>[
|
||||
hoverTarget,
|
||||
const ModalBarrier(dismissible: false),
|
||||
],
|
||||
);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
// Start out of hoverTarget
|
||||
await gesture.moveTo(const Offset(100, 100));
|
||||
|
||||
await tester.pumpWidget(subject);
|
||||
// Move into hoverTarget and tap
|
||||
await gesture.down(const Offset(5, 5));
|
||||
await tester.pumpWidget(subject);
|
||||
await gesture.up();
|
||||
await tester.pumpWidget(subject);
|
||||
|
||||
// Move out
|
||||
await gesture.moveTo(const Offset(100, 100));
|
||||
await tester.pumpWidget(subject);
|
||||
|
||||
expect(hovered, isFalse,
|
||||
reason: 'because the hover is not prevented by ModalBarrier');
|
||||
});
|
||||
|
||||
testWidgets('ModalBarrier does not prevent interactions with widgets in front of it', (WidgetTester tester) async {
|
||||
final Widget subject = Stack(
|
||||
textDirection: TextDirection.ltr,
|
||||
@@ -89,6 +132,37 @@ void main() {
|
||||
reason: 'because the drag is prevented by ModalBarrier');
|
||||
});
|
||||
|
||||
testWidgets('ModalBarrier does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async {
|
||||
final Widget subject = Stack(
|
||||
textDirection: TextDirection.ltr,
|
||||
children: <Widget>[
|
||||
const ModalBarrier(dismissible: false),
|
||||
hoverTarget,
|
||||
],
|
||||
);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
addTearDown(gesture.removePointer);
|
||||
// Start out of hoverTarget
|
||||
await gesture.moveTo(const Offset(100, 100));
|
||||
await tester.pumpWidget(subject);
|
||||
expect(hovered, isFalse);
|
||||
|
||||
// Move into hoverTarget
|
||||
await gesture.moveTo(const Offset(5, 5));
|
||||
await tester.pumpWidget(subject);
|
||||
expect(hovered, isTrue,
|
||||
reason: 'because the hover is prevented by ModalBarrier');
|
||||
hovered = false;
|
||||
|
||||
// Move out
|
||||
await gesture.moveTo(const Offset(100, 100));
|
||||
await tester.pumpWidget(subject);
|
||||
expect(hovered, isTrue,
|
||||
reason: 'because the hover is prevented by ModalBarrier');
|
||||
hovered = false;
|
||||
});
|
||||
|
||||
testWidgets('ModalBarrier pops the Navigator when dismissed by primay tap', (WidgetTester tester) async {
|
||||
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||
'/': (BuildContext context) => FirstWidget(),
|
||||
|
||||
@@ -523,7 +523,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
Transform.scale(
|
||||
scale: 2.0,
|
||||
child: const MouseRegion(),
|
||||
child: const MouseRegion(opaque: false),
|
||||
),
|
||||
);
|
||||
final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion));
|
||||
@@ -534,10 +534,12 @@ void main() {
|
||||
// transform.)
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
|
||||
|
||||
// Test that needsCompositing updates correctly with callback change
|
||||
await tester.pumpWidget(
|
||||
Transform.scale(
|
||||
scale: 2.0,
|
||||
child: MouseRegion(
|
||||
opaque: false,
|
||||
onHover: (PointerHoverEvent _) {},
|
||||
),
|
||||
),
|
||||
@@ -550,13 +552,27 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
Transform.scale(
|
||||
scale: 2.0,
|
||||
child: const MouseRegion(),
|
||||
child: const MouseRegion(opaque: false),
|
||||
),
|
||||
);
|
||||
expect(listener.needsCompositing, isFalse);
|
||||
// TransformLayer for `Transform.scale` is removed again as transform is
|
||||
// executed directly on the canvas.
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
|
||||
|
||||
// Test that needsCompositing updates correctly with `opaque` change
|
||||
await tester.pumpWidget(
|
||||
Transform.scale(
|
||||
scale: 2.0,
|
||||
child: const MouseRegion(
|
||||
opaque: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(listener.needsCompositing, isTrue);
|
||||
// Compositing is required, therefore a dedicated TransformLayer for
|
||||
// `Transform.scale` is added.
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
|
||||
});
|
||||
|
||||
testWidgets("Callbacks aren't called during build", (WidgetTester tester) async {
|
||||
@@ -942,6 +958,42 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('an empty opaque MouseRegion is effective', (WidgetTester tester) async {
|
||||
bool bottomRegionIsHovered = false;
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) { bottomRegionIsHovered = true; },
|
||||
onHover: (_) { bottomRegionIsHovered = true; },
|
||||
onExit: (_) { bottomRegionIsHovered = true; },
|
||||
child: Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
const MouseRegion(opaque: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(location: const Offset(20, 20));
|
||||
addTearDown(gesture.removePointer);
|
||||
|
||||
await gesture.moveTo(const Offset(5, 5));
|
||||
await tester.pump();
|
||||
await gesture.moveTo(const Offset(20, 20));
|
||||
await tester.pump();
|
||||
expect(bottomRegionIsHovered, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async {
|
||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||
RenderMouseRegion().debugFillProperties(builder);
|
||||
|
||||
Reference in New Issue
Block a user